Thursday, March 12, 2015

Adding a GUI to a PowerShell Script to Fix Folder Dates


This is a follow up to my original post about building a single-line Windows PowerShell script to change folder modification dates (the Folder Modification Dates are updated to reflect the newest file in the folder). PowerShell is a scripting language for Windows that provides access to a wide range of system resources for automating and managing local and network resources. It is worth a quick read to get an overview of PowerShell.

http://en.wikipedia.org/wiki/Windows_PowerShell

To my knowledge PowerShell is the only way to to modify the LastWriteTime of a folder in Windows. It seems that other programming languages that implement this feature must do so with a call the Shell object. So I thought let's just do it in PowerShell to start with.

The single-line script works but requires hard-coding the path to the directory to be processed and provides no feedback while the script is running. The single-line script also has a tiny hiccup with empty folders but we will talk more about that shortly. I want to make the script more friendly by providing a dialog box to select the folder to be processed and showing a progress bar while the folders are being processed.

PowerShell User Interface Elements

As always, my first stop was a search on Google. I started looking for ways to implement user interface elements in PowerShell. It took a few days of a couple of hours here and there to find what I wanted. Most dialog boxes have several options and much of my effort was just getting the correct options or buttons present (or hidden) in the user prompts.

When searching for code examples I found that similar tasks were often approached in very different ways. I kept searching until I felt I was not seeing new approaches for a particular element. I then worked to select the code fragments that implemented the feature I wanted in the fewest steps or in the way that was easiest to understand. At times I removed features I did not want and only re-used the minimum code needed for my goals.

I also used the O'Reilly book, "Windows PowerShell Pocket Reference" when implementing a couple of details and cleaning up my code. I avoided breaking lines to make the code easier to read. I feel that while distracting, long lines make the code easier to follow once opened in a window large enough to avoid wrapping the lines.

The Power of PowerShell

One of the most powerful aspects of PowerShell is the ability to use Get- cmdlets to generate a collection of objects that can be piped to another cmdlet, like Where-Object or ForEach-Object, for additional processing. These two combined with the recursion built into PowerShell with the -recurse switch provide an amazing level of flexibly.

My goal was the most readable code, not the shortest or fastest. I intentionally avoided using shortcut names for the cmdlets in the strip to make it more clear what was being done.

The Result

The final version of the script prompts the user to select a folder to be processed and then asks you to verify the target folder before starting. Then a progress bar appears but does not respond for a few seconds to several (many?) seconds. (The delay is due to the initial recursion of the folder and sub-folders). I would like to make the progress bar responsive during the initial folder scan but could not find an easy way to show progress while doing the initial folder recursion. After the initial recursion we know how many folders need to be processed so we can easily reflect our progress.) Once all of the folders are processed a dialog box tells you how many folders were processed.

The Script

Here is a link to the project on GitHub. Just grab the file FolderDateFixGUI.ps1 and run it from within the WindowPowerShell ISE. Adjust your Execution Policy if needed.

SetFolderDates

Below is a copy of the code for your browsing pleasure. But it looks ugly in the browser with the line breaks. It is much easier to read if you pull the file from GitHub and view in the PowerShell ISE or Notepad++.

#Change Folder Modification Dates to the date of the newest file in each folder.
#To use this you must first run powershell.exe as admin and update the Execution Policy using the command below.
#> Set-ExecutionPolicy RemoteSigned
#Confirm that you want to make the change when prompted
#You can then run the script from the Windows PowerShell ISE
#Shane Trent, shanedtrent@gmail.com, fettricks.blogspot.com

Clear  #clear console text
$FolderNav = New-Object -com Shell.Application  # Create application object to be used to display navigation box
$BrowseFolderOptions = 513  #Options, &h200 (512, no New Folder Button) + &h1 (1, File system directories only)
$folder = $FolderNav.BrowseForFolder(0, "Select Folder for processing.", $BrowseFolderOptions, "") #Display nav box and get folder to process
if ($folder.Self.Path) #Proceed only if a folder was selected, exit if cancelled
  { 
  $ConfirmPath=[System.Windows.Forms.MessageBox]::Show($folder.Self.Path,"Confirm Folder to be processed.",[System.Windows.Forms.MessageBoxButtons]::OKCancel)
  switch ($ConfirmPath)    #Have the user to confirm the path to the folder to be processed
    {
    "OK" 
      { 
      Write-Host "Folder Path Selected - " $folder.Self.Path    #Print the selected folder to the console
      Set-Location $folder.Self.Path                            #Set the path to the folder to be processed
      Write-Progress -Activity "Scanning Folder..."  -Status "Please wait."     #Show static status bar before scanning the folder tree
      $FolderArray = Get-ChildItem -recurse | Where-Object {$_.PsIsContainer}  #Scan tree to generate an array of all folders
      for ($i=0;> $i -lt $FolderArray.count; $i++)    #Process each folder
        { 
        $NewestFile = ($FolderArray[$i] | Get-ChildItem -recurse | Where-Object  {!$_.PsIsContainer} | Sort-Object LastWriteTime | Select-Object -last 1) # Find newest file in the tree
        if ($NewestFile) {$FolderArray[$i].LastWriteTime = $NewestFile.LastWriteTime}               #If there is a file, update the folder date
        $d = [decimal]::>round(($i /$FolderArray.count) * 100)                       #Calculate the percentage of folders processed for bar and status text
        Write-Progress -Activity "Processing.." -PercentComplete $d -CurrentOperation "$d% complete" -Status "Please wait."  #Update progress bar
        } 
      Write-Host "Processing Completed." "Processed" ($FolderArray.count) "folders!"  #We are done, write stats to console
      Write-Progress "Done" "Done" -completed                                         #Close the progress bar
      $message = ($FolderArray.count) , " Folders were processed."                    #Build message for final message box
      [System.Windows.Forms.MessageBox]::Show($message , "Processing Completed!")     #Display message box with number of folders processed
      }
    "Cancel" 
       {
       Write-Host "You cancelled the selected folder path"  #We land here if the user cancelled the path confirmation message box                                      
       }
     }
   }
else {Write-Host "You cancelled the folder selection."}     #And we land here if the user cancelled the folder navigation box


Testing 

The best, and safest, way to test the script is to make a test copy of your target folder (Projects, Photos, etc). I recommend making a backup copy anyway. Burn a copy to optical disk while you are thinking about it.

Now open the new test folder that you just created and select detailed view and sort by date. On my Windows 7 system all of the just created files still have their correct original modification dates but the folders in the new copy will have the current date and time for the Date Modified field. So viewing the folders by date provides no useful information! But we can fix this!

Now run the script from the PowerShell ISE and select the your new test folder as the target folder. After processing refresh your view of the folder and the folders should now be sorted by the date of the most recently modified file in each folder! This allows you to tell at a glance when you last worked on the project in each folder.

Walk Through

I decided to add a code walk-though to provide a little more detail on a few of the steps. I initally wanted to skip a walk through, it is very dull if you just want to use the script as is. And after working with PowerShell for a little while the code starts to seem self explanatory. But PowerShell can be a little confusing when you are getting started so I added this walk through.

The first active line of the script on line 8 clears the console

Next we create anew Shell.Application object that we will use to let the user select the folder.

Then we set the folder options to show the desired options in the dialog box to select the folder. This was one of the harder items to get set just the way I wanted. For one thing, I did not want the user to be able to create a new folder.

On line 11 the user select the target folder and the path to that folder is stored in $folder. The user may cancel the folder selection which would store a null in $folder.

Now we are down to line 12 in the PowerShell ISE listing and we start doing some data checking. First we check $folder to see that the user did select a folder. If a folder was selected, we proceed to line 14. If the user canceled the folder selection we jump to line 41 and we are done.

Next is line 14 where we present the user with a MessageBox with two buttons (OK and Cancel) and ask them to confirm the path to the target folder. I wanted to make sure we work on the correct folder! This line returns either "OK or "Cancel".

Line 15 uses a Switch statement to see if the user confirmed the path, if so continue to line 19. If the user cancels the path we exit to line 35.

On line 19 we write the path to the console just to leave a paper trail.

Then we set the current working directory to the desired path with Set-Location.

Line 21 sets up a progress bar with the desired text.

We start the heavy lifting on line 22 with the Get-ChildItem -recurse that generates a list of every folder and file in the entire folder tree for the target folder. That list of files and folders is piped via the "|" to the cmdlet Where-Object {$.PsIsContainer} that returns only the objects that are containers (folders).

The magic starts on line 23 where we set up a For loop to process each folder.

On line 25 we start processing the folders one at a time. There is a lot going on in this line that allows us to find the newest file in the current folder. We start with the current folder, $FolderArray[$i] and do a recursive Get-ChildItem to get a list of all folders and files. The list of files and folders is filtered to exclude folders, leaving only files. The list of file is sorted from oldest to newest and finally the last object, the newest file is selected. So now $NewestFile is set equal to the the newest file.

Line 26 addresses the one glitch from the single line script. If you recall, the single-line script worked with empty folders but would write an error to the console for each empty folder. The new script avoids this problem by checking first to see if a newest file exists in the folder! If there are no files in the folder then $NewestFile will be a null and the IF will be false. If $NewestFile is not a null, then we set the LastWriteTime of the current folder equal to the LastWriteTime of the $NewestFile.

Line 27 calculates the percent of folders that we have finished processing.

Line 28 updates the progress bar with our current percent completion. This is the end of the For loop so the we return to line 23 where the index is incremented and we process the next folder!

We reach line 30 when we have processed all of the folders. Here we write a note to the console recording the number of folders processed.

Line 31 updates and closes the progress bar.

Line 32 builds a message box with the number of folders processed.

Line 33 displays the message box and declares the processing complete!

The remaining lines are reached only when the user has canceled the folder selection or canceled the specified folder path. Each writes a line to the console to indicate why processing was halted.

Wrap

I hope you find the script useful for processing folder dates or just seeing how to set some of the user interface options in PowerShell!

Thanks for reading!

Shane

No comments:

Post a Comment