FileWatcher.ps1
<#PSScriptInfo .VERSION 1.0 .GUID 74bfbe4f-e53b-41b3-9819-360a585b68ae .AUTHOR francisconabas@outlook.com .COMPANYNAME .COPYRIGHT .TAGS File Copy File Watcher .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES FileWatcher.ps1 is composed of the following features: Logging compatible with CMTrace. Invoke-FileCopy function. with MD5 hash verification, copying only unique files. New-FileWatcher function. Creates a File Watcher and register it for monitoring only or with file copy. Invoke-LogCleaner function. Manages the logs generated by the solution. .PRIVATEDATA #> <# .DESCRIPTION Solution uses FileSystemWatch class to monitor a directory and copy files when detects a change, if desired. #> [CmdletBinding(DefaultParameterSetName = 'Monitor')] param ( [Parameter (Mandatory = $false, ParameterSetName = 'Monitor', HelpMessage = 'Log file path location. Without log file name.')] [Parameter (Mandatory = $false, ParameterSetName = 'InvokeCopy', HelpMessage = 'Log file path location. Without log file name.')] [ValidateNotNullOrEmpty()] [string] $LogFilePath = "$Env:windir\Logs", [Parameter (Mandatory = $false, ParameterSetName = 'Monitor', HelpMessage = 'File Watcher events to monitor.')] [Parameter (Mandatory = $false, ParameterSetName = 'InvokeCopy', HelpMessage = 'File Watcher events to monitor.')] [ValidateNotNullOrEmpty()] [array] $WatcherEvents = @('Changed','Created','Deleted','Disposed','Error','Renamed'), [Parameter (Mandatory = $true, ParameterSetName = 'Monitor', HelpMessage = 'Path to monitor.')] [Parameter (Mandatory = $true, ParameterSetName = 'InvokeCopy', HelpMessage = 'Path to monitor.')] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter (Mandatory = $false, ParameterSetName = 'Monitor', HelpMessage = 'File Watcher filter. Default is *')] [Parameter (Mandatory = $false, ParameterSetName = 'InvokeCopy', HelpMessage = 'File Watcher filter. Default is *')] [ValidateNotNullOrEmpty()] [string] $Filter, [Parameter (Mandatory = $false, ParameterSetName = 'Monitor', HelpMessage = 'Destination for the file copy. Designed to be used with -Copy switch.')] [Parameter (Mandatory = $true, ParameterSetName = 'InvokeCopy', HelpMessage = 'Destination for the file copy. Designed to be used with -Copy switch.')] [ValidateNotNullOrEmpty()] [string] $CopyDestination, [Parameter (Mandatory = $false, ParameterSetName = 'InvokeCopy', HelpMessage = 'Triggers file copy when any of the File Watcher events occour.')] [switch] $Copy ) #region Functions Function Global:Add-Log { param ( [Parameter (Mandatory = $true)] [string] $LogValue, [Parameter (Mandatory = $true)] [ValidateSet("Info", "Warning", "Error")] [string] $Type, [Parameter (Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Component ) switch ($Type) { "Info" { [int]$Type = 1 } "Warning" { [int]$Type = 2 } "Error" { [int]$Type = 3 } } $Source = $MyInvocation.MyCommand.Name $Content = "<![LOG[$LogValue]LOG]!>" +` "<time=`"$(Get-Date -Format "HH:mm:ss.ffffff")`" " +` "date=`"$(Get-Date -Format "M-d-yyyy")`" " +` "component=`"$Component`" " +` "context=`"$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " +` "type=`"$Type`" " +` "thread=`"$([Threading.Thread]::CurrentThread.ManagedThreadId)`" " +` "file=`"$Source`">" if (!(Test-Path $LogFilePath -ErrorAction SilentlyContinue)) { mkdir $LogFilePath | Out-Null } try { Add-Content -Path "$LogFilePath\CustomFileWatcher.log" -Value $Content -Force -ErrorAction Stop } catch { Start-Sleep -Milliseconds 700 Add-Content -Path "$LogFilePath\CustomFileWatcher.log" -Value $Content -Force } } function Global:Invoke-FileCopy { [CmdletBinding()] param ( [Parameter (Mandatory = $false)] [ValidateRange([int]30, [int]::MaxValue)] [int] $DaysToDelete = 30, [Parameter (Mandatory = $true, Position = 0)] [string] $Source, [Parameter (Mandatory = $true, Position = 1)] [string] $Destination ) #region DirectoryCheck Write-Verbose "File copy triggered. Source: $Source. Dest: $Destination. $(Get-Date)." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "File copy triggered. Source: $Source. Dest: $Destination." if (!(Test-Path -Path $Destination)) { mkdir $Destination -Force | Out-Null } try { $Files = Get-ChildItem $Source -Filter *.* -Recurse -Force -ErrorAction Stop } catch { Add-Log -Type 'Error' -Component 'FileCopy' -LogValue "Error fetching files. $($_.Exception.Message)" return Write-Error "Error fetching files. $($_.Exception.Message)" } if ($Files) { $ExtMgt = $Files | Group-Object -Property Extension -NoElement Write-Verbose "Directory Found. $($Files.Count) Files found." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Directory Found. $($Files.Count) Files found." foreach ($Ext in $ExtMgt) { Write-Verbose "$($Ext | Select-Object -ExpandProperty Count) Files with extension $($Ext.Name)." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "$($Ext.Count) Files with extension $($Ext.Name)." } } else { Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "No files found on the given directory." return Write-Warning "No files found on the given directory. $(Get-Date)." } #endregion #region CleanOldFiles Write-Verbose "Cleaning files older than $DaysToDelete days. $(Get-Date)." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Cleaning files older than $DaysToDelete days." $OCopiedFiles = Get-ChildItem $Destination -Filter *.* -Recurse -Force -ErrorAction SilentlyContinue if ($OCopiedFiles) { foreach ($File in $OCopiedFiles) { if (((Get-Date) - $File.CreationTime).Days -ge $DaysToDelete) { try { Remove-Item $File.FullName -Force -ErrorAction Stop } catch { Write-Warning "Error removing file $($File.Name). $($_.Exception.Message) $(Get-Date)." Add-Log -Type 'Warning' -Component 'FileCopy' -LogValue "Error removing file $($File.Name). $($_.Exception.Message)" } } } } #endregion #region CopyFiles foreach ($File in $Files) { if ($OCopiedFiles) { if ($OCopiedFiles.Name -contains $File.Name) { Write-Verbose "Found file with name $($File.Name) on destination. Checking hash. $(Get-Date)." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Found file with name $($File.Name) on destination. Checking hash." $SameName = $OCopiedFiles | Where-Object {$_.Name -eq $File.Name} if ((Get-FileHash $File.FullName).Hash -eq (Get-FileHash $SameName.FullName).Hash) { Write-Verbose "Both files with same MD5 hash. Skipping copy. $(Get-Date)." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Both files with same MD5 hash. Skipping copy." } else { Write-Verbose "Files with different MD5 hash. Copying with new name. $(Get-Date)." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Files with different MD5 hash. Copying with new name." try { Copy-Item -Path $File.FullName -Destination "$Destination\$($File.Name)_$(Get-Date -Format 'MM-dd-yyyy_hh.mm.ss').$($File.Extension)" -Force -ErrorAction Stop } catch { Write-Warning "Error copying file $($File.Name). $($_.Exception.Message) $(Get-Date)." Add-Log -Type 'Warning' -Component 'FileCopy' -LogValue "Error copying file $($File.Name). $($_.Exception.Message)" } } } else { Write-Verbose "File $($File.Name) not found on destination. Copying. $(Get-Date)." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "File $($File.Name) not found on destination. Copying." try { Copy-Item -Path $File.FullName -Destination $Destination -Force -ErrorAction Stop } catch { Write-Warning "Error copying file $($File.Name). $($_.Exception.Message) $(Get-Date)." Add-Log -Type 'Warning' -Component 'FileCopy' -LogValue "Error copying file $($File.Name). $($_.Exception.Message)" } } } else { Write-Verbose "No files on destination. Copying. $(Get-Date)." Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "No files on destination. Copying." try { Copy-Item -Path $File.FullName -Destination $Destination -Force -ErrorAction Stop } catch { Write-Warning "Error copying file $($File.Name). $($_.Exception.Message) $(Get-Date)." Add-Log -Type 'Warning' -Component 'FileCopy' -LogValue "Error copying file $($File.Name). $($_.Exception.Message)" } } } #endregion } function New-FileWatcher { [cmdletbinding()] param ( [Parameter (Mandatory = $true)] [string] $MonitorPath, [Parameter (Mandatory = $false)] [string] $MonitorFilter ) try { $FullPathName = (Get-Item $MonitorPath -Force -ErrorAction Stop).FullName } catch { Add-Log -Type 'Error' -Component 'New-FileWatcher' -LogValue "Error trying to find path specified. $($_.Exception.Message)" return Write-Error "Error trying to find path specified. $($_.Exception.Message) $(Get-Date).)" } try { $FileWatcher = New-Object System.IO.FileSystemWatcher $FileWatcher.Path = $FullPathName if ($Filter) { $FileWatcher.Filter = $MonitorFilter } $FileWatcher.IncludeSubdirectories = $true $FileWatcher.EnableRaisingEvents = $true Add-Log -Type 'Info' -Component 'New-FileWatcher' -LogValue "File Watcher created for pass: $FullPathName." Write-Verbose "File Watcher created for path: $FullPathName. $(Get-Date)." if ($Copy) { $Action = { $Path = $Event.SourceEventArgs.FullPath $Type = $Event.SourceEventArgs.ChangeType $SourcePathName = $Event.MessageData.FullPathName $DestinationPathName = $Event.MessageData.CopyDestination Add-Log -Type 'Info' -Component 'FileWatch' -LogValue "$Type detected on $Path. Triggering file copy." Write-Host "$Type detected on $Path. Triggering file copy. $(Get-Date)." -ForegroundColor DarkCyan Invoke-FileCopy -Source $SourcePathName -Destination $DestinationPathName -Verbose } } else { $Action = { $Path = $Event.SourceEventArgs.FullPath $Type = $Event.SourceEventArgs.ChangeType Add-Log -Type 'Info' -Component 'FileWatch' -LogValue "$Type detected on $Path. 'Copy' switch not called. Monitoring only." Write-Host "$Type detected on $Path. 'Copy' switch not called. Monitoring only. $(Get-Date)." -ForegroundColor DarkCyan } } $MessageObject = New-Object PsObject -Property @{LogFilePath = $LogFilePath; FullPathName = $FullPathName; CopyDestination = $CopyDestination} foreach ($WEvent in $WatcherEvents) { Register-ObjectEvent $FileWatcher "$WEvent" -Action $Action -MessageData $MessageObject | Out-Null Add-Log -Type 'Info' -Component 'New-FileWatcher' -LogValue "Object event registered for '$WEvent'." Write-Verbose "Object event registered for '$WEvent'. $(Get-Date)." } } catch { Add-Log -Type 'Error' -Component 'New-FileWatcher' -LogValue "Unable to set File Watcher. $($_.Exception.Message)" return Write-Error "Unable to set File Watcher. $($_.Exception.Message) $(Get-Date)." } } function Invoke-LogCleaner { param ( [Parameter (Mandatory = $true)] [string] $CLPath ) Write-Verbose 'Cleaning logfiles older than 7 days.' Add-Log -Type 'Info' -Component 'CleaningLogs' -LogValue 'Cleaning logfiles older than 7 days.' $LogCTime = (Get-ChildItem $CLPath -ErrorAction SilentlyContinue).CreationTime if ($LogCTime) { $LogFCreated = Get-Date($LogCTime) $DateTime = Get-Date if (($DateTime - $LogFCreated).Days -ge 7) { try { Remove-Item $CLPath -Force -ErrorAction Stop } catch { Write-Warning 'Error removing old logfile. Manual intervention needed.' Add-Log -Type "Warning' -Component 'CleaningLogs' -LogValue 'Error removing old logfile. Manual intervention needed. $($_.Exception.Message)." } } else { Write-Verbose 'Logfile newer than 7 days.' Add-Log -Type 'Info' -Component 'CleaningLogs' -LogValue 'Logfile newer than 7 days.' } } else { Write-Verbose 'Logfile not found.' Add-Log -Type 'Warning' -Component 'CleaningLogs' -LogValue 'Logfile not found.' } } #endregion if (!$Filter) { New-FileWatcher -MonitorPath $Path -Verbose } else { New-FileWatcher -MonitorPath $Path -MonitorFilter $Filter -Verbose } Invoke-LogCleaner -CLPath "$LogFilePath\CustomFileWatcher.log" Invoke-FileCopy -Source (Get-Item $Path -Force).FullName -Destination $CopyDestination -Verbose while ($true) { Start-Sleep -Seconds 5 } |