File system watcher Event when run using a script - powershell

Hi I have installed the PowerShellPack and I am using the FileSystem watcher module,but the problem when I safe the file as a script and execute it.
The problem is that if you execute the script it runs and the monitors the folder for changes but once the script stops (gets to the end of execution) the folder is no longer monitored.
I have tried to place everything in a do while loop but that does not seem to work.
PowerShellPack Install
Import-Module -Name FileSystem
$TempCopyFolder = "c:\test"
$PatchStorage = "c:\testpatch"
Start-FileSystemWatcher -File $TempCopyFolder -Do {
$SearchPath = $File
$PatchesPath = $PatchStorage
$NewFolderFullPath = "$($eventArgs.FullPath)"
$NewFolderName = "$($eventArgs.Name)"
$PathToCheck = "$PatchesPath\$NewFolderName"
#Check if it is a filde or folder
switch ($ObjectType)
{{((Test-Path $NewFolderFullPath -PathType Container) -eq $true)}{$ObjectType = 1;break}
{((Test-Path $NewFolderFullPath -PathType Leaf) -eq $true)}{$ObjectType = 2;break}}
# Its a folder so lets check if we have a folder in the $PatchesPath already
IF($ObjectType -eq 1){
IF(!(Test-Path -LiteralPath $PathToCheck -EA 0))
{
sleep -Seconds 3
#Make a new directory where we store the patches
New-item -Path $PatchesPath -Name $NewFolderName -ItemType directory
#Make a folde in the folder for TC1
$TcFolder=$NewFolderName + '_1'
$NewPatchesPath = "$PatchesPath\$NewFolderName"
New-item -path $NewPatchesPath -Name $TcFolder -ItemType directory
$CopySrc = $NewFolderFullPath
$CopyDes = "$NewPatchesPath\$TcFolder"
}
# There is a folder there so lets get the next number
Else{
$HighNumber = Get-ChildItem -Path $PathToCheck | select -Last 1
#Core_SpanishLoginAttemptsConfiguration_Patch_03
$NewNumber = [int](Select-String -InputObject $HighNumber.Name -Pattern "(\d\d|\d)" | % { $_.Matches } | % { $_.Value } )+1
$TcFolder= $NewFolderName + '_' + $NewNumber
$NewPatchesPath = "$PatchesPath\$NewFolderName"
$CopySrc = $NewFolderFullPath
$CopyDes = "$NewPatchesPath\$TcFolder"
}
#Lets copy the files to their new home now that we know where every thing goes
$robocopy = "robocopy.exe"
$arguments = '''' + $CopySrc + '''' +' '+ ''''+ $CopyDes + '''' + '/E'
Invoke-Expression -Command "$robocopy $arguments"
Do {sleep -Seconds 1;$p = Get-Process "robo*" -ErrorAction SilentlyContinue}
While($p -ne $null)
#Now lets check every thing copyed
$RefObj = Get-ChildItem -LiteralPath $NewFolderFullPath -Recurse
$DifObj = Get-ChildItem -LiteralPath $CopyDes -Recurse
IF(Compare-Object -ReferenceObject $RefObj -DifferenceObject $DifObj)
{write-host "Fail"}
Else{# Now lets delete the source
Remove-Item -LiteralPath $CopySrc -Force -Recurse
}
}}

You don't need add-on modules or WMI for this. Just set of the FileSystemWatcher yourself and register an event. Sure, it's a bit more code, but at least you know what's going on. :)
$watcher = new-object System.IO.FileSystemWatcher
$watcher.Path = 'c:\logs'
$watcher.Filter = '*.log' # whatever you need
$watcher.IncludeSubDirectories = $true # if needed
$watcher.EnableRaisingEvents = $true
Register-ObjectEvent $watcher -EventName Changed -SourceIdentifier 'Watcher' -Action { param($sender, $eventArgs)
<process event here>
}
When done:
Unregister-Event -SourceIdentifier 'Watcher'

This is something you probably need: Monitoring file creation using WMI and PowerEvents module
PowerEvents Module isn't mandatory if you know how to create Permanent Events in WMI. For more information on that, check my eBook on WQL via PowerShell: http://www.ravichaganti.com/blog/?page_id=2134

Related

Why is powershell switching directories mid code on copy-item when the variable is already defined?

I have some code that checks a target file, waits for a change, and I want it to only move the most recent files based on their LastWriteTime Value. However, every time I change a file within the target directory nothing is copying over and I am having the copy-item directory change to "C:\Users\run". I
it recognizes that there are files to copy and even states their filename when throwing the error. What can I do in this situation to make sure my copy-item command is copying from my target directory?
Code for Reference:
$File = "C:\Users\run\Desktop\Target"
$destinationFolder = "c:\users\run\desktop\dest"
$maxDays = "-1"
$maxMins = "20"
$date = Get-Date
Write-Host "Waiting For File To Change in Job Cloud..."
$Action = '
dateChecker
Write-Host "Moving Files From Job Cloud To Server Shares... Please Do Not Disrupt This Service"
write-host "files copied to job cloud..."
exit
'
$global:FileChanged = $false
function dateChecker {
Foreach($File in (Get-ChildItem -Path $File)){
if($File.LastWriteTime -lt ($date).AddMinutes($maxMins)){
Write-Host "Moving Files From Job Cloud To Server Shares... Please Do Not Disrupt This Service"
Copy-Item -Path $File -Destination $destinationFolder -recurs #-ErrorAction #silentlyContinue
}
}
}
while($true) {
function Wait-FileChange {
param(
[string]$File,
[string]$Action
)
$FilePath = Split-Path $File -Parent
$FileName = Split-Path $File -Leaf
$ScriptBlock = [scriptblock]::Create($Action)
$Watcher = New-Object IO.FileSystemWatcher $FilePath, $FileName -Property #{
IncludeSubdirectories = $false
EnableRaisingEvents = $true
}
$onChange = Register-ObjectEvent $Watcher Changed -Action {$global:FileChanged = $true}
while ($global:FileChanged -eq $false){
Start-Sleep -Milliseconds 100
}
& $ScriptBlock
Unregister-Event -SubscriptionId $onChange.Id
}
Wait-FileChange -File $File -Action $Action
}
PowerShell is not switching directories - although I can certainly see why you'd think that based on the behavior. The explanation is closer than you might think though:
The -Path parameter takes a [string] argument.
$File is not a string - it's a [FileInfo] object - and PowerShell therefore converts it to a string before passing it to Copy-Item -Path. Unfortunately, this results in the name of the file (not the full path) being passed as the argument, and Copy-Item therefore has to resolve the full path, and does so relative to the current working directory.
You can fix this by passing the full path explicitly to Copy-Item -LiteralPath:
Copy-Item -LiteralPath $File.FullName ... |...
or you can let the pipeline parameter binder do it for you by piping the $File object to Copy-Item:
$File |Copy-Item ... |...
Why -LiteralPath instead of -Path? -Path accepts wildcard patterns like filenameprefix[0-9] and tries to resolve it to a file on disk, meaning if you have to operate on files with [ or ] in the name, it'll result in some unexpected behavior :)

Powershell/WScript Create Shortcut if not exist

I'm wondering if someone can help me? I've butchered a few powershell scripts I've found online that make shortcuts from $source to $destination. However, it appears to overwrite each time, and I only want it to create a .lnk on new.
The original source of the script is here and this is my current "non working" script.. I added the following, but it doesn't seem to work. I think I need to somehow get it to check the $destination and then continue if $file.lnk doesn't exist
If ($status -eq $false) {($WshShell.fso.FileExists("$Destination") + "*.lnk")
Full script:
function Create-ShortcutForEachFile {
Param(
[ValidateNotNullOrEmpty()][string]$Source,
[ValidateNotNullOrEmpty()][string]$Destination,
[switch]$Recurse
)
# set recurse if present
if ($Recurse.IsPresent) { $splat = #{ Recurse = $true } }
# Getting all the source files and source folder
$gci = gci $Source #splat
$Files = $gci | ? { !$_.PSisContainer }
$Folders = $gci | ? { $_.PsisContainer }
# Creating all the folders
if (!(Test-Path $Destination)) { mkdir $Destination -ea SilentlyContinue > $null }
$Folders | % {
$Target = $_.FullName -replace [regex]::escape($Source), $Destination
mkdir $Target -ea SilentlyContinue > $null
}
# Creating Wscript object
$WshShell = New-Object -comObject WScript.Shell
# Creating all the Links
If ($status -eq $false) {($WshShell.fso.FileExists("$Destination") + "*.lnk")
$Files | % {
$InkName = "{0}.lnk" -f $_.sBaseName
$Target = ($_.DirectoryName -replace [regex]::escape($Source), $Destination) + "\" + $InkName
$Shortcut = $WshShell.CreateShortcut($Target)
$Shortcut.TargetPath = $_.FullName
$Shortcut.Save()
}
}
}
Create-ShortcutForEachFile -Source \\myserver.domain.local\Folder1\Folder2\Test -Destination \\myserver2.domain.local\Folder1\Folder2\Test -Recurse
Hoping anyone can help me out, apologies for being a powershell/scripting noob.
My brother kindly reworked the script to suit better to my needs.
Here it is:
#################################################
<#
CREATE-SHORTCUT - creates shortcut for all files from a source folder
version : 1.0
Author :
Creation Date :
Modified Date :
#>
#------------------------------------------------------------[ variables ]----------------------------------------------------------
$sourceDir="D:\scripts\create-shortcut\source"
$targetDir="D:\scripts\create-shortcut\dest"
#-------------------------------------------------------------[ Script ]-----------------------------------------------------------
# get files/files from folder
$src_gci=Get-Childitem -path $sourceDir -Recurse
$src_files=$src_gci | ? { !$_.PSisContainer }
$src_folders=$src_gci | ? { $_.PSisContainer }
# create subfolders
$src_folders | Copy-Item -Destination { join-path $targetDir $_.Parent.FullName.Substring($sourceDir.Length) } -Force
# create shortcuts
$WshShell = New-Object -comObject WScript.Shell
$src_files | % {
$lnkName="{0}.lnk" -f $_.BaseName
$Target = ($_.DirectoryName -replace [regex]::escape($sourceDir), $targetDir) + "\" + $lnkName
$Shortcut = $WshShell.CreateShortcut($Target)
$Shortcut.TargetPath = $_.FullName
$Shortcut.Save()
# change to SourceFiles ModifiedDate #
$src_date=$_.LastWriteTime
Get-ChildItem $Target | % { $_.LastWriteTime = "$src_date" }
}

Powershell - How to rename a file after moving it?

I am currently working on a project that requires that I move a file, and then rename it. I am using this code to move it and that is working. However, the rename portion is not taking place as it should. I cannot figure out why this isn't working. What have I goofed up? I have been beating my head against my desk for at least 20 minutes trying to figure this out.
# Variables for Watcher
$folder = "C:\Program Files\Whatever\Connector\Export\JobStatus"
$filter = '*.txt'
$date=(get-date -Format d) -replace("/")
$time=(get-date -Format t) -replace(":")
# Watcher + Settings
$fsw = New-Object IO.FileSystemWatcher $folder, $filter
$fsw.IncludeSubdirectories = $false
$fsw.NotifyFilter = [IO.NotifyFilters]'FileName', 'DirectoryName'
# Register Event (when file is created)
$onCreated = Register-ObjectEvent $fsw Created -SourceIdentifier FileCreated
-Action {
# Foreach file loop
ForEach ($f in $fsw)
{
if (($File = Get-Item $Event.SourceEventArgs.FullPath | select -Expand
Extension) -eq ".txt")
{
#Used for file testing - Opens the text file for 10 secs, then kills
it.
#Start-Process -FilePath $Event.SourceEventArgs.FullPath | %{ sleep
10; $_ } | kill
# Variables for move
$folderpath = ($Event.SourceEventArgs.FullPath | Split-Path)
$folderfile = ($Event.SourceEventArgs.FullPath | Split-Path -Leaf)
$destination = "C:\Program Files\Whatever\Connector\Staging\"
$newname = "job.import.$date"+"_"+"$time.txt"
}
# Variables for logging
$logpath = 'C:\Program
Files\Whatever\Connector\Export\JobStatus\outlog.txt'
# Grab current file and move to "Staging" folder
try
{
Get-ChildItem -Path $folderpath -Filter $folderfile | Move-Item -
Destination $destination | sleep 5 | Write-Host Rename-Item
$destination$folderfile -NewName $newname | Out-File -FilePath $logpath -
Append
Write-Host $destination$newname
#sleep 5
#Rename-Item "$destination $folderfile" -NewName $newname
#Write-Host $destination $folderfile
#"File $folderfile renamed to $newname" | Out-File -FilePath
$logpath -Append
# Log the move in logfile
"File $folderfile moved to $destination" | Out-File -FilePath
$logpath -Append
}
# Log if errors + clear
catch
{
$error | Out-File -FilePath $logpath -Append
$Error.Clear()
}
}
}
The pipeline is broken when there's no object output. move-item doesn't output an object unless the -passthru parameter is used. Also, set-sleep doesn't output anything. So, rename-item is never reached.
Replace the pipes after move-item and sleep with semicolons, and it should work.
I actually fixed this removing the piped rename and replacing it with a 5 second sleep. I do the rename after the sleep and it works fine now. Still not sure why the rename wasnt working in the piped command though.

Moving Files with certain File Name

I wrote a script which detects a folder called Post within the subfolders of W:\SUS Test2 and moves CSV file which has "EMMain" in the filename. But the problem I have is the CSV files doesn't copy to the destination folder W:\SUS Test3 it copies it to W:\SUS Test2 folder.
$path = "W:\SUS Test2\"
$destination = "W:\SUS Test3\"
$fsw = New-Object System.IO.FileSystemWatcher $path -Property #{
IncludeSubdirectories = $true
NotifyFilter = [IO.NotifyFilters]'DirectoryName
}
$createdLTC = Register-ObjectEvent $fsw -EventName Created -Action {
$item = Get-Item $eventArgs.FullPath
if ($item.Name -like "Post") {
Get-ChildItem $item -filter "*EMMain*" | Copy-Item -dest $destination
}
}
You are trying to access the $destination variable in a portion of code that it is not available in. The script block in your Register-OjbectEvent will not have access to this variable. Try declaring $destination in the script block directly.
$createdLTC = Register-ObjectEvent $fsw -EventName Created -Action {
$destination = "W:\SUS Test3\"
$item = Get-Item $eventArgs.FullPath
If ($item.Name -like "Post") {
Get-ChildItem $item -filter "*EMMain*" | Copy-Item -dest $destination
}
}

Powershell - "The process cannot access the file because it is being used by another process"

Below is a script that monitors a directory and its subfolders for deposited files. Every 10 minutes or so, I look for new files and then match them against a database table that tell me where they need to be moved to - then it copies the files to a local archive, moves them to the locations they need to be moved to, and inserts a record into another database table with the file's attributes and where it came and went. If there is no match in the database - or there is an script error - it sends me an email.
However, since files are getting deposited to the directory constantly, it's possible that a file is still being written when the script executes. As a result, I get the error The process cannot access the file because it is being used by another process. emailed to me all the time. In addition, because I'm not dealing with the error up front; it goes through the loop and a false entry is inserted into my log table in the database with incorrect file attributes. When the file finally frees up, it gets inserted again.
I'm looking for a way to identify files that have processes attached to them; and skipping them when the script executes - but several days of web searches and some testing hasn't yielded an answer yet.
## 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 <SQLMail#bar.com>"
$email_recip = [STRING]"foo#bar.com"
$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
}
You can use the SysInternals handles.exe to find the open handles on a file. The exe can be downloaded from 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
Outputs:
WINWORD.EXE pid: 3744 type: File 1A0: C:\Users\me\Downloads\The-DSC-Book.docx
Alternatively, you can check for errors either via try/catch or by looking at the $error collection after the Move-Item attempt then handle the condition appropriately.
$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
}
To expand on Arluin's answer. It fails if there's spaces in either the handle.exe or the $targetfile.
This will work for spaces in both and also formats the result to give you the Program Name.exe
$targetfile = "W:\Apps Folder\File.json"
$result = & "W:\Apps (Portable)\handle.exe" "$targetfile" | Select-String ([System.IO.Path]::GetFileNameWithoutExtension($targetfile))
$result = $result -replace '\s+pid\:.+'
$result
# PS> FreeCommander.exe
One way to avoid file locks caused by running the script on a timer is to use an event driven approach using a file system watcher. It has the ability to execute code when an event such as a new file is created in the folder you are monitoring.
To run code when the file is finished copying you would need to listen for the changed event. There is a slight issue with this event in that it fires once when the file begins copying and again when it is finished. I got an idea to work around this chicken/egg problem after checking out the module Mike linked to in the comments. I've updated the code below so that it will only fire off code when file has fully been written.
To try, change $folderToMonitor to the folder you want to monitor and add some code to process the file.
$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