PowerShell Pipeline
Tracking Changes to a Folder Using PowerShell
Using the FileSystemWatcher class will aid in documenting even the slightest changes.
Whether it is monitoring for files and folders being updated in a specific location or you want to set up a sort of Dropbox to dump files in, the options for doing any sort of monitoring against a folder (or subfolders) are very slim. You could have a scheduled task configured that could run against a folder after so many minutes and compare the contents vs. a baseline file such as a CSV or XML file. While this would certainly work, there is another way to accomplish this that provides a more real time approach as using the FileSystemWatcher class.
Using the FileSystemWatcher class, we can set up a listener against a folder and its subfolders while specifying narrow scopes of what we are looking to watch such as whether the file/folder was created or deleted as well as if the ACL on the objects have been changed. Now before you start thinking that we might have an amazing monitoring solution that can provide who did what to what, the type of data returned is minimal such as listing the name of the item as well as the previous name if it was renamed. I also mentioned that this could be used in conjunction with a Dropbox folder that could run as a consumer and handle various files dumped into the folder.
Let's take a look at the FileSystemWatcher object and examine both its properties and events that are available. The Events are the key to understanding how we can use this for monitoring using Register-ObjectEvent.
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher | Get-Member -Type Properties,Event
I'm mostly concerned about the Events in this instance. We have the following Events available to use:
Name |
Description |
Changed |
Occurs when a file or directory in the specified Path is changed. |
Created |
Occurs when a file or directory in the specified Path is created. |
Deleted |
Occurs when a file or directory in the specified Path is deleted. |
Error |
Occurs when the instance of FileSystemWatcher is unable to continue monitoring changes or when the internal buffer overflows. |
Renamed |
Occurs when a file or directory in the specified Path is renamed. |
I'm not really concerned about the Error Event, but left it on the list for reference purposes. In the case of my dropbox idea, I am only concerned about the Created event because this will get tripped each time and item is copied or moved to the location. Now onto the properties of this object.
$FileSystemWatcher
The properties that you should only be worried about are: NotifyFilter, Filter, IncludeSubdirectories and Path.
The Filter property is basically just specifying what kind of items you are looking for. It only allows for a single string to be used and you cannot use something like "*.txt|*.xls" as it is not supported.
IncludeSubdirectories gives you the recursive monitoring of subfolders underneath the Path that you specified.
NotifyFilter is where we can specify the type of properties to look at on a file or folder such as the Name or ACL of an item. The table below provides more information in regards to this.
Name |
Description |
Attributes |
The attributes of the file or folder. |
CreationTime |
The time the file or folder was created. |
DirectoryName |
The name of the directory. |
FileName |
The name of the file. |
LastAccess |
The date the file or folder was last opened. |
LastWrite |
The date the file or folder last had anything written to it. |
Security |
The security settings of the file or folder. |
Size |
The size of the file or folder. |
Using this knowledge, I am going to track for incoming items for my Dropbox folder and state if they have been created. Nothing too crazy, just a simple example to show how this works.
$FileSystemWatcher.Path = "C:\Users\proxb\Desktop\DropBox"
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Created -Action {
$Object = "{0} was {1} at {2}" -f $Event.SourceEventArgs.FullPath,
$Event.SourceEventArgs.ChangeType,
$Event.TimeGenerated
$WriteHostParams = @{
ForegroundColor = 'Green'
BackgroundColor = 'Black'
Object = $Object
}
Write-Host @WriteHostParams
}
I now have a PSJob available which won't actually start until the first event is tripped. When it has tripped, we will get a message on the console showing the name of the item as well as the type of action that took place. Since we are only monitoring for created items, the only action that will be shown are creations. I'll go ahead and drop some items into this folder and see what transpires.
Here we see all of the items that have been added to the Dropbox folder. If we had a more useful Action scriptblock, we could have each item handled differently based on the file extension.
To make things a little easier, I wrote a function called New-FileSystemWatcher function that will help to build out multiple watchers with less effort:
Function Start-FileSystemWatcher {
[cmdletbinding()]
Param (
[parameter()]
[string]$Path,
[parameter()]
[ValidateSet('Changed','Created','Deleted','Renamed')]
[string[]]$EventName,
[parameter()]
[string]$Filter,
[parameter()]
[System.IO.NotifyFilters]$NotifyFilter,
[parameter()]
[switch]$Recurse,
[parameter()]
[scriptblock]$Action
)
#region Build FileSystemWatcher
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
If (-NOT $PSBoundParameters.ContainsKey('Path')){
$Path = $PWD
}
$FileSystemWatcher.Path = $Path
If ($PSBoundParameters.ContainsKey('Filter')) {
$FileSystemWatcher.Filter = $Filter
}
If ($PSBoundParameters.ContainsKey('NotifyFilter')) {
$FileSystemWatcher.NotifyFilter = $NotifyFilter
}
If ($PSBoundParameters.ContainsKey('Recurse')) {
$FileSystemWatcher.IncludeSubdirectories = $True
}
If (-NOT $PSBoundParameters.ContainsKey('EventName')){
$EventName = 'Changed','Created','Deleted','Renamed'
}
If (-NOT $PSBoundParameters.ContainsKey('Action')){
$Action = {
Switch ($Event.SourceEventArgs.ChangeType) {
'Renamed' {
$Object = "{0} was {1} to {2} at {3}" -f $Event.SourceArgs[-1].OldFullPath,
$Event.SourceEventArgs.ChangeType,
$Event.SourceArgs[-1].FullPath,
$Event.TimeGenerated
}
Default {
$Object = "{0} was {1} at {2}" -f $Event.SourceEventArgs.FullPath,
$Event.SourceEventArgs.ChangeType,
$Event.TimeGenerated
}
}
$WriteHostParams = @{
ForegroundColor = 'Green'
BackgroundColor = 'Black'
Object = $Object
}
Write-Host @WriteHostParams
}
}
#endregion Build FileSystemWatcher
#region Initiate Jobs for FileSystemWatcher
$ObjectEventParams = @{
InputObject = $FileSystemWatcher
Action = $Action
}
ForEach ($Item in $EventName) {
$ObjectEventParams.EventName = $Item
$ObjectEventParams.SourceIdentifier = "File.$($Item)"
Write-Verbose "Starting watcher for Event: $($Item)"
$Null = Register-ObjectEvent @ObjectEventParams
}
#endregion Initiate Jobs for FileSystemWatcher
}
Using my function, I will begin watching the Dropbox folder for created files and will note the extensions that could then help in handling what to do with each file.
$FileSystemWatcherParams = @{
Path = 'C:\Users\Proxb\Desktop\Dropbox'
Recurse = $True
NotifyFilter = 'FileName'
Verbose = $True
Action= {
$Item = Get-Item $Event.SourceEventArgs.FullPath
$WriteHostParams = @{
ForegroundColor = 'Green'
BackgroundColor = 'Black'
}
Switch -regex ($Item.Extension) {
'\.(ps1|psm1|psd1)' {$WriteHostParams.Object = "Processing PowerShell file: $($Item.Name)"}
'\.(docx|doc)' {$WriteHostParams.Object = "Processing Word document: $($Item.Name)"}
'\.(xlsx|xls)' {$WriteHostParams.Object = "Processing Excel spreadsheet: $($Item.Name)"}
'\.csv' {$WriteHostParams.Object = "Processing CSV spreadsheet: $($Item.Name)"}
'\.xml' {$WriteHostParams.Object = "Processing XML document: $($Item.Name)"}
'\.exe' {$WriteHostParams.Object = "Processing Executable: $($Item.Name)"}
'\.onepkg' {$WriteHostParams.Object = "Processing OneNote package: $($Item.Name)"}
'\.lnk' {$WriteHostParams.Object = "Processing Link: $($Item.Name)"}
'\.cer|\.pfx' {$WriteHostParams.Object = "Processing Certificate File: $($Item.Name)"}
Default{$WriteHostParams.Object = "Processing File: $($Item.Name)"}
}
$Item | Remove-Item
Write-Host @WriteHostParams
}
}
@('Created') | ForEach {
$FileSystemWatcherParams.EventName = $_
Start-FileSystemWatcher @FileSystemWatcherParams
}
In this case, I am removing each item to simulate each file being consumed and then removed after the event action block is done with it. But you can notice that by having a well-defined Action block allows me the flexibility of handling each file sent to the drop box so it can be consumed to further automate my system.
That is all for today's article on using the FileSystemWatcher to monitor and respond to file/folder events. Drop me a comment and let me know how you are using this technique in your day to day operations!