This is part 2, of my attempt to "sync" photos I deleted on my desktop to my SD card (you can read part 1 first). In this post, I try to use PowerShell with .NET framework to (try) monitor for file system changes, and output to a batch file that "replicates" the ren
(rename) and del
(delete) to files and folders.
Script to Monitor Delete and Rename Events
PowerShell can invoke methods from .NET, so I thought of using the FileSystemWatcher APIs. Here is my first version to:
- unregister any prior events registered,
- create a FileSystemWatcher object,
- register a function to handle files and folders deleted events,
- register a function to handle files and folders renamed events.
You shouldn't have a file called run_changes.cmd
already, unless you really want to append to it.
watch_deletes.ps1
:
Unregister-Event FileDeleted -ErrorAction SilentlyContinue
Unregister-Event FileRenamed -ErrorAction SilentlyContinue
$folder = pwd
$filter = '*.*'
$global:output = "$folder\run_changes.cmd"
Write-Host "Monitoring $folder\$filter, output to $output"
$fsw = New-Object IO.FileSystemWatcher $folder, $filter -Property @{
IncludeSubdirectories = $true;
NotifyFilter = "FileName,DirectoryName"
}
Register-ObjectEvent $fsw Deleted -SourceIdentifier FileDeleted -Action {
if ($Event.SourceEventArgs.FullPath -ine $output) {
$name = $Event.SourceEventArgs.Name
$change = $Event.SourceEventArgs.ChangeType
$time = $Event.TimeGenerated
Write-Host "$time $change ""$name""" -fore red
Out-File $output -Encoding ASCII -Append -InputObject "del ""$name"""
}
}
Register-ObjectEvent $fsw Renamed -SourceIdentifier FileRenamed -Action {
if ($Event.SourceEventArgs.OldFullPath -ine $output) {
$oldname = $Event.SourceEventArgs.OldName
$name = $Event.SourceEventArgs.Name
$time = $Event.TimeGenerated
Write-Host "$time Renamed ""$oldname"" to ""$name""" -fore white
Out-File $output -Encoding ASCII -Append -InputObject "ren ""$oldname"" ""$name"""
}
}
Easy! This script is clean and works well. But moves are not tracked - specifically Windows treats the move as a delete followed by a create.
Script to Monitor "Move" Events
So this is my revised script to try and "associate" a delete event followed by a create event as as a "move". This is tricky because it could be a legitimate delete followed by an unrelated create!
I can't vouch for this script. It's very hack-y and relies on an arbitrary timeout of 500 ms for this "association" - I don't know if this is suitable or not!
You can see how my watch_changes.ps1
script:
- sets-up some global variables, since events are triggered in a background thread,
- sets-up a timer object to wait for a delete and create pair,
- handles files and folders differently (i.e.
del
to delete a file vsrmdir
to delete a folder), - handles events by appending the change detected to an output file.
You shouldn't have a file called run_changes.cmd
already, unless you really want to append to it.
param (
[switch]$stop = $false,
$out = "run_changes.cmd"
)
Unregister-Event WatchTimerElapsed -ErrorAction SilentlyContinue
Unregister-Event WatchFileDeleted -ErrorAction SilentlyContinue
Unregister-Event WatchFileCreated -ErrorAction SilentlyContinue
Unregister-Event WatchFileRenamed -ErrorAction SilentlyContinue
Unregister-Event WatchFolderDeleted -ErrorAction SilentlyContinue
Unregister-Event WatchFolderCreated -ErrorAction SilentlyContinue
Unregister-Event WatchFolderRenamed -ErrorAction SilentlyContinue
if ($stop) { exit }
$folder = pwd
$filter = '*.*'
$global:output = "$folder\$out"
$global:filedeleted = ""
$global:folderdeleted = ""
Write-Host "Monitoring $folder\$filter, output to $output"
$timer = New-Object Timers.Timer
$timer.Interval = 500
Register-ObjectEvent $timer Elapsed -SourceIdentifier WatchTimerElapsed -Action {
$timer.Stop()
if ($filedeleted -ne "") {
Out-File $output -Encoding ASCII -Append -InputObject "del ""$filedeleted"""
$global:filedeleted = ""
}
if ($folderdeleted -ne "") {
Out-File $output -Encoding ASCII -Append -InputObject "rmdir ""$folderdeleted"""
$global:folderdeleted = ""
}
}
$fsw = New-Object IO.FileSystemWatcher $folder, $filter -Property @{
IncludeSubdirectories = $true;
NotifyFilter = "FileName"
}
Register-ObjectEvent $fsw Deleted -SourceIdentifier WatchFileDeleted -Action {
if ($Event.SourceEventArgs.FullPath -ine $output) {
$name = $Event.SourceEventArgs.Name
$change = $Event.SourceEventArgs.ChangeType
$time = $Event.TimeGenerated
Write-Host "$time $change ""$name""" -fore red
if ($filedeleted -ne "") {
Out-File $output -Encoding ASCII -Append -InputObject "del ""$filedeleted"""
}
$global:filedeleted = $name
$timer.Start()
}
}
Register-ObjectEvent $fsw Created -SourceIdentifier WatchFileCreated -Action {
if ($Event.SourceEventArgs.FullPath -ine $output) {
$name = $Event.SourceEventArgs.Name
$change = $Event.SourceEventArgs.ChangeType
$time = $Event.TimeGenerated
Write-Host "$time $change ""$name""" -fore blue
if ($filedeleted -ne "") {
Out-File $output -Encoding ASCII -Append -InputObject "move ""$filedeleted"" ""$name"""
$global:filedeleted = ""
} else {
Out-File $output -Encoding ASCII -Append -InputObject "#created ""$name"""
}
}
}
Register-ObjectEvent $fsw Renamed -SourceIdentifier WatchFileRenamed -Action {
if ($Event.SourceEventArgs.OldFullPath -ine $output) {
$oldname = $Event.SourceEventArgs.OldName
$name = $Event.SourceEventArgs.Name
$time = $Event.TimeGenerated
Write-Host "$time Renamed ""$oldname"" to ""$name""" -fore white
Out-File $output -Encoding ASCII -Append -InputObject "ren ""$oldname"" ""$name"""
}
}
$dsw = New-Object IO.FileSystemWatcher $folder, $filter -Property @{
IncludeSubdirectories = $true;
NotifyFilter = "DirectoryName"
}
Register-ObjectEvent $dsw Deleted -SourceIdentifier WatchFolderDeleted -Action {
if ($Event.SourceEventArgs.FullPath -ine $output) {
$name = $Event.SourceEventArgs.Name
$change = $Event.SourceEventArgs.ChangeType
$time = $Event.TimeGenerated
Write-Host "$time $change ""$name""" -fore red
if ($folderdeleted -ne "") {
Out-File $output -Encoding ASCII -Append -InputObject "rmdir ""$folderdeleted"""
}
$global:folderdeleted = $name
$timer.Start()
}
}
Register-ObjectEvent $dsw Created -SourceIdentifier WatchFolderCreated -Action {
if ($Event.SourceEventArgs.FullPath -ine $output) {
$name = $Event.SourceEventArgs.Name
$change = $Event.SourceEventArgs.ChangeType
$time = $Event.TimeGenerated
Write-Host "$time $change ""$name""" -fore blue
if ($folderdeleted -ne "") {
Out-File $output -Encoding ASCII -Append -InputObject "move ""$folderdeleted"" ""$name"""
$global:folderdeleted = ""
} else {
Out-File $output -Encoding ASCII -Append -InputObject "mkdir ""$name"""
}
}
}
Register-ObjectEvent $dsw Renamed -SourceIdentifier WatchFolderRenamed -Action {
if ($Event.SourceEventArgs.OldFullPath -ine $output) {
$oldname = $Event.SourceEventArgs.OldName
$name = $Event.SourceEventArgs.Name
$time = $Event.TimeGenerated
Write-Host "$time Renamed ""$oldname"" to ""$name""" -fore white
Out-File $output -Encoding ASCII -Append -InputObject "ren ""$oldname"" ""$name"""
}
}
Running the PowerShell Script
Now, to run the script from the Command Prompt (cmd
) in the folder where my photos reside before performing any renames or deletes:
cd my_photos
powershell -NoExit -ExecutionPolicy ByPass -File watch_changes.ps1
You need -NoExit
because the script needs to keep running in the background. PowerShell won't close until you use exit
, to stop the FileSystemWatcher.
I explained the reason for -ExecutionPolicy
in part 1 - it's to allow unsigned scripts to be executed.
An alternative is to globally allow unsigned scripts:
- From the Start Menu > right click Windows PowerShell ISE (x86) > Run as administrator.
- In the top section, enter
Set-ExecutionPolicy RemoteSigned
. - Now hit F5 (or from the Debug menu > Run) then select Yes to confirm - and now scripts can run unsigned.
This is not safe, so to undo this, enter Set-ExecutionPolicy Restricted
and hit Run again...
The script has one parameter, -stop
to simply stop monitoring events and exit.
Running the PowerShell Script... again
Anyway, run it, and you'll get an output like this:
Windows PowerShell
Copyright (C) 2009 Microsoft Corporation. All rights reserved.
Monitoring C:\Dev\*.*, output to C:\Dev\run_changes.cmd
WARNING: column "Command" does not fit into the display and was removed.
Id Name State HasMoreData Location
-- ---- ----- ----------- --------
1 WatchFileTimer NotStarted False
2 WatchFileDel... NotStarted False
3 WatchFileCre... NotStarted False
4 WatchFileRen... NotStarted False
When files are deleted or moved, the console will show the events the script detected, e.g.
05/20/2018 12:30:45 Deleted "test\IMG1.jpg"
05/20/2018 12:30:48 Created "IMG1.jpg"
05/20/2018 12:31:24 Deleted "IMG1.jpg"
05/20/2018 12:33:56 Created "IMG2.jpg"
05/20/2018 12:39:44 Renamed "IMG2.jpg" to "IMG3.jpg"
Once you exit, the run_changes.cmd
file will contain commands to try and repeat the changes made on the SD Card, e.g.
move "test\IMG1.jpg" "IMG1.jpg"
del "IMG1.jpg"
#created "IMG2.jpg"
ren "IMG2.jpg" "IMG3.jpg"
Note that of course my script doesn't replicate a file created on my desktop folder to the SD card. I could replace the commented out note #created
to instead copy the file from the source folder to the SD card, it's absolutely possible. I couldn't be bothered to, since I don't need my script to handle new files!
I move the cmd
script to my SD card (the same folder where the files reside) and then run it...
Conclusion
PowerShell can easly call .NET framework APIs, but the API doesn't handle "moves". Therefore, I don't trust my script, instead preferring to stick to my much simpler script in part 1...