FileWatcher.ps1


<#PSScriptInfo
 
.VERSION 1.1
 
.GUID 74bfbe4f-e53b-41b3-9819-360a585b68ae
 
.AUTHOR francisconabas@outlook.com
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS File Copy File Watcher
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
Change to only monitor on "Delete" event
Added path handling for running the script with a different user than the one to monitor.
Added "Replace" switch on Invoke-FileCopy to replace files with same name and different hash.
Changed the renaming method for keeping the both files to match Windows standard.
 
.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')]
    [Parameter  (Mandatory = $false, ParameterSetName = 'InvokeCopy')]
    [ValidateNotNullOrEmpty()]
    [string]    $LogFilePath = "$Env:windir\Logs",

    [Parameter  (Mandatory = $false, ParameterSetName = 'Monitor')]
    [Parameter  (Mandatory = $false, ParameterSetName = 'InvokeCopy')]
    [ValidateNotNullOrEmpty()]
    [array]     $WatcherEvents = @('Changed','Created','Deleted','Disposed','Error','Renamed'),

    [Parameter  (Mandatory = $true, ParameterSetName = 'Monitor')]
    [Parameter  (Mandatory = $true, ParameterSetName = 'InvokeCopy')]
    [ValidateNotNullOrEmpty()]
    [string]    $Path,

    [Parameter  (Mandatory = $false, ParameterSetName = 'Monitor')]
    [Parameter  (Mandatory = $false, ParameterSetName = 'InvokeCopy')]
    [ValidateNotNullOrEmpty()]
    [string]    $Filter,

    [Parameter  (Mandatory = $false, ParameterSetName = 'Monitor')]
    [Parameter  (Mandatory = $true, ParameterSetName = 'InvokeCopy')]
    [ValidateNotNullOrEmpty()]
    [string]    $CopyDestination,

    [Parameter  (Mandatory = $false, ParameterSetName = 'InvokeCopy')]
    [switch]    $Copy,

    [Parameter  (Mandatory = $false, ParameterSetName = 'InvokeCopy')]
    [switch]    $Replace

)

#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,

        [Parameter  (Mandatory = $false)]
        [ValidateSet('Directory','Leaf')]
        [string]    $PathType = 'Directory',

        [Parameter  (Mandatory = $false)]
        [switch]    $FileReplace = $Replace

    )   

    #region DirectoryCheck
    switch ($PathType) {
        'Directory' {
            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)"
                throw 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 {
                            if ($FileReplace) {
                                Write-Verbose "Files with different MD5 hash. Replace switch CALLED. Replacing file on destination. $(Get-Date)."
                                Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Files with different MD5 hash. Replace switch CALLED. Replacing file on destination."
                                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 "Files with different MD5 hash. Replace switch NOT called. Copying with new name. $(Get-Date)."
                                Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Files with different MD5 hash. Replace switch NOT called. Copying with new name."
                                try {
                                    $Index = 0
                                        $DestinationFile = "$Destination\$($File.Name)"
                                        while (Test-Path $DestinationFile -PathType Leaf -ErrorAction SilentlyContinue) {
                                            $Index ++
                                            $DestinationFile = "$Destination\$($File.BaseName)($Index)$($File.Extension)"
                                        }
                                        Copy-Item -Path $File.FullName -Destination $DestinationFile -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
        }
        'Leaf' {
            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 {
                $File = Get-Item $Source -Force -ErrorAction Stop
            }
            catch {
                Add-Log -Type 'Error' -Component 'FileCopy' -LogValue "Error fetching files. $($_.Exception.Message)"
                return Write-Warning "Error fetching files. $($_.Exception.Message)"
            }
            #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

            if ($File) {
                #region CopyFiles
                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 {
                            if ($FileReplace) {
                                Write-Verbose "Files with different MD5 hash. Replace switch CALLED. Replacing file on destination. $(Get-Date)."
                                Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Files with different MD5 hash. Replace switch CALLED. Replacing file on destination."
                                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 "Files with different MD5 hash. Replace switch NOT called. Copying with new name. $(Get-Date)."
                                Add-Log -Type 'Info' -Component 'FileCopy' -LogValue "Files with different MD5 hash. Replace switch NOT called. Copying with new name."
                                try {
                                    $Index = 0
                                    $DestinationFile = "$Destination\$($File.Name)"
                                    while (Test-Path $DestinationFile -PathType Leaf -ErrorAction SilentlyContinue) {
                                        $Index ++
                                        $DestinationFile = "$Destination\$($File.BaseName)($Index)$($File.Extension)"
                                    }
                                    Copy-Item -Path $File.FullName -Destination $DestinationFile -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
            }
            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)."
            }
        }
        Default {
            throw "Path type $PathType not supported."
        }
    }
    
}

function New-FileWatcher {
    
    [cmdletbinding()]
    param (

        [Parameter  (Mandatory = $true)]
        [string]    $MonitorPath,

        [Parameter  (Mandatory = $false)]
        [string]    $MonitorFilter

    )

    try {
        $FullPathName = (Get-LegalPath -InputPath $MonitorPath -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
                if ($Type -eq 'Deleted') {
                    Add-Log -Type 'Info' -Component 'FileWatch' -LogValue "$Type detected on $Path. Monitoring only."
                    Write-Host "$Type detected on $Path. Monitoring only. $(Get-Date)." -ForegroundColor DarkCyan
                }
                else {
                    $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.'
    }
    
}

function Get-LegalPath {

    [CmdletBinding()]
    param (
    
        [Parameter  (Mandatory = $true)]
        [string]    $InputPath,

        [Parameter  (Mandatory = $false)]
        [switch]    $SessionUser

    )

    if ($SessionUser) {
        try {
            $PathItem = Get-Item $InputPath -ErrorAction Stop | Select-Object *
            if (!$PathItem) {
                Add-Log -Type 'Error' -Component 'Get-LegalPath' -LogValue "Path not found for $InputPath."
                throw "Path not found for $InputPath. $(Get-Date)."
            }
            else {
                return $PathItem
            }
        }
        catch {
            Add-Log -Type 'Error' -Component 'Get-LegalPath' -LogValue "Error finding path $InputPath. $($_.Exception.Message)"
            throw "Error finding path $InputPath. $($_.Exception.Message) $(Get-Date)."
        }
    }
    else {
        if ($InputPath -like '*C:\Users*') {
            $LoggedUser = (Get-CimInstance Win32_ComputerSystem).UserName -replace 'CONCENTRIX\\'
            $FinalPathString = $InputPath -replace "(?<=Users\\)(.*?)(?=\\)", $LoggedUser
            try {
                $PathItem = Get-Item $FinalPathString -ErrorAction Stop | Select-Object *
                if (!$PathItem) {
                    Add-Log -Type 'Error' -Component 'Get-LegalPath' -LogValue "Path not found for $FinalPathString."
                    throw "Path not found for $FinalPathString. $(Get-Date)."
                }
                else {
                    return $PathItem
                }
            }
            catch {
                Add-Log -Type 'Error' -Component 'Get-LegalPath' -LogValue "Error finding path $FinalPathString. $($_.Exception.Message)"
                throw "Error finding path $FinalPathString. $($_.Exception.Message) $(Get-Date)."
            }
        }
        else {
            try {
                $PathItem = Get-Item $InputPath -ErrorAction Stop | Select-Object *
                if (!$PathItem) {
                    Add-Log -Type 'Error' -Component 'Get-LegalPath' -LogValue "Path not found for $InputPath."
                    throw "Path not found for $InputPath. $(Get-Date)."
                }
                else {
                    return $PathItem
                }
            }
            catch {
                Add-Log -Type 'Error' -Component 'Get-LegalPath' -LogValue "Error finding path $InputPath. $($_.Exception.Message)"
                throw "Error finding path $InputPath. $($_.Exception.Message) $(Get-Date)."
            }
        }
    }
    
}
#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
}