A PowerShell Script for Cleaning Your Drive – Part 1

3 May

Clean-Disk

A Windows computer system needs periodic cleaning.  Windows comes equipped the “Disk Clean Manager”. They recently added a new “Storage Sense” option in settings to automatically clear out some of those old junk files.  And yet, tools like CCleaner thrive.  The tools provided by Microsoft simply aren’t stellar.  Frankly, they don’t need to be.  They’ve covered the basics.  Those of us that want more have plenty of alternatives.

Today, I have another PowerShell script to share with you.  I wrote this as a simple alternative to CCleaner.  I’m not trying to dissuade you from using CCleaner by any means.  I’m a fan of several of Piriform’s products.  If you have the budget for it, the CCleaner Network product for business is the way to go.  It does far more than can be done effectively by any script.  If that’s not you, I think you will find this script useful.

Without further adieu

I have the newest version of this code posted on my github site.  You probably should want to know how it works before you start haphazardly deleting files.  Keep on reading for the more technical details and usage examples.

Parameters

What better way to show the parameters, than with the help comments.   I few of these do warrant special comment.  Because the function uses [CmdletBinding(SupportsShouldProcess)] it supports the -Verbose and -WhatIf parameters.  I depend heavily on these parameters throughout the code.  The primary role of the script is to delete data.  Deletion is always risky business without proper testing which makes the extra what-if code worthwhile.

.PARAMETER Mode
 User or System. Determines the time of action performed for many of the other parameters.
 System Mode requires administrative permissions.
 
 .PARAMETER TheWorks
 System Mode Performs an aggressive clean with 1 parameter. Equivalent to -ResetCompStore 
 -EmptyWinLogs -EmptyWinUpdDownload -EmptyRecycleBin -EmptyUserTemp -EmptyWinTemp 
 -EmptyTempInternet -EmptyRDPCache -EmptyUserDownload
 
 .PARAMETER RetentionTime
 Integer. Set the number of days to retain during empty. 
 Defaults to 7 Days.
 
 .PARAMETER ReportStats
 Switch. Reports the space Freed.

.PARAMETER LogPath
 String. Enables logging to path specified.
 
 .PARAMETER ResetCompStore
 System Mode Integrate previous updates into current store and clear the update folder of archives. 
 You will not be able to rollback previously installed updates, but you free up a lot of space.

.PARAMETER EmptyWinLogs
 System Mode Removes CBS persist logs and DISM logs.
 A frequent need to clear the persist logs indicates that you have a problem with Windows.

.PARAMETER EmptyWinDumpFiles
 System Mode Removes memory dump files.
 Removing a dump file reduces the ability to troubleshoot a crash. It is not recommended on machines with issues.

.PARAMETER RemoveChkDskFragments
 System Mode Removes fragment files created by chkdsk.
 Removing a dump file reduces the ability to troubleshoot a crash. It is not recommended on machines with issues.

.PARAMETER EmptyWinUpdDownload
 System Mode Switch to empty the Download folder for Windows Update. Respects "RetentionTime" Parameter.
 This should always be run with or after ResetCompStore parameter. Used too frequently will impede WU performance.

.PARAMETER RemoveWinOld
 System Mode Switch to delete the Windows.Old folder left behind after large updates.
 You will not be able to roll back to the previous version of Windows, but you free up a lot of space.

.PARAMETER EmptyRecycleBin
 User Mode Empty the Recycle Bin for the current user. Respects "RetentionTime" Parameter.
 System Mode Empty the Recycle Bin for all users. Respects "RetentionTime" Parameter.

.PARAMETER EmptyUserTemp
 User Mode Empty the current user's temporary folder. Respects "RetentionTime" Parameter.
 System Mode Empty all users' temporary folder. Respects "RetentionTime" Parameter.

.PARAMETER EmptyWinTemp
 System Mode Empty the Windows Temporary folder. Do not run this too frequently. Respects "RetentionTime" Parameter.

.PARAMETER EmptyTempInternet
 User Mode Empty IE, Chrome, and Firefox temporary internet files for the current user.
 System Mode Empty IE, Chrome, and Firefox temporary internet files for all users.
 Edge browser uses Windows Temp folder.

.PARAMETER EmptyRDPCache
 User Mode Remove old RDP cache files from current user profile. Respects "RetentionTime" Parameter.
 System Mode Remove old RDP cache files from all user profiles. Respects "RetentionTime" Parameter.
 Not advised with 0 retention time while currently connected to RDP Session.

.PARAMETER EmptyUserDownload
 User Mode Switch to empty the Download folder for the current user. Respects "RetentionTime" Parameter.
 System Mode Switch to empty the Download folder for all users. Respects "RetentionTime" Parameter.

.PARAMETER RemoveJunkFolders
 System Mode Attempts to find and removed junk folders left behind by installers. Respects "RetentionTime" Parameter.
 Uses Regex to identify folders. There is a possibility that root folders not present on most machines could get caught up.
 It is suggested to test this with -WhatIf before rolling it out to automation.

.PARAMETER OtherFilesFolders
 String Array. Array of files to remove. Be mindful of mode used with respect to the files being removed.

.PARAMETER DiskCleanTool
 Specifies a SageRun value to run the Windows Disk Cleanup Tool with (see https://support.microsoft.com/en-us/kb/253597).
 This option is a little safer than some of the more forceful methods of this script.
 
.PARAMETER WhatIf
 Dry Run.

The preliminary bits

The first thing to do is start logging.  This is not quite as simple as outputting a bunch of text.  First, I allow the user to enter the path.   And we users take things, such as a temp folder on the root, as the understood norm (back in the day, that was the norm).  Regardless, assuming the supplied path is valid will be wrong too often, so take a simple shot at creating the directory, if it’s not already present.  Even that is risky, so I closed it all in a catch-try, just in case. The second issue is the already running transcript.  Transcript logging in PowerShell is wonderfully easy to use, but killing your script prematurely because you forgot to add the WhatIf does not stop the transcription.  There is no way to see if a transcript currently running except to attempt a start or a stop.  So, a catch-try here attempts a stop before starting the transcript with verbose output in the finally block.

# Start Logging.
 if($LogPath)
 {
     try
     {
         $logFolder = Split-Path $LogPath -Parent
         if (!(Test-Path $logFolder))
         {
             New-Item -ItemType Directory -Force -Path $logFolder
         }
         # Lets attempt a stop, just in case you hit Ctrl+C as soon as you started the first time around.
         try
         {
             Stop-Transcript | Out-Null
         }
         catch
         {}
         finally
         {
             $oVP = $VerbosePreference
             $VerbosePreference = 'continue'
             $oWP = $WarningPreference
             $WarningPreference = 'continue'
             $transcript = Start-Transcript -Path $LogPath -Force
         }
     }
     catch
     {
         Write-Verbose $_.Exception.Message
     $LogPath = $null
     }
 }

A few simple items here.  Some of the math for file retention can be done upfront.  The next section is rather important.  If you attempt to execute the function without administrative permissions, it switches to “User” mode so there is still some benefit.  Alternatively, I could have exited with a single error.  There really needs to be some action here to prevent the plethora of errors that would result from attempting to delete all of those “system” files without permissions.  Lastly, a little output for the logs or verbose output.

# Set Retention Values
 $retentionDate = (Get-Date).AddDays(-$RetentionTime)
 # Make sure we have admin permissions
 if ($Mode -eq 'System' -and !([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")))
 {
     $Mode = 'User'
     Write-Verbose "Cannot run in System mode without administrative permissions."
 }
 Write-Verbose "Executing in $($Mode) Mode."
 Write-Verbose "Cleaning files/folders older than $retentionDate."

WMI Performance If the ReportStats parameter is used we need take a quick measure of the drive space. I used the Win32_Volume while popular option seems to favor Win32_LogicalDisk. The only value I really need to report on is space and the Win32_Volume class is a great deal faster.  A quick regex and select gives me an object with only what I need plus one new column I will use later.

# Gather Stats
 if ($ReportStats)
 {
 $reportData = Get-WmiObject Win32_Volume | Where-Object {
 $_.Name -match '\D:\\' } | Select-Object Name, FreeSpace, NewFreeSpace
 }
A helper function to do the hard work

The last chunk of my Begin block is a bit more involved.  System files take a few steps to remove, even with Administrative permissions.  This little helper function allows me to perform the following steps without repeating a bunch of code.

  1. The “System” owns these files and folders.  Before I can do anything else, I need to take ownership of the item.
  2. Owning the item does not necessarily give me full control of the item, or in the case of directories, the items inside.  I need to set the permissions, and for directories, I also need to enable inheritance.
  3. System files still don’t like to be deleted so I strip off all of the attributes before attempting a removal.
  4. Even though the tools I plan to use have “recursive” options, they will not get all of the files on the first go.  It takes a couple of passes to make sure we get them all.

I don’t need to worry about the cmdletbinding because this attribute is inherited but defining the parameter is important because I want to use this function as part of a pipeline.  I’ve explicitly defined the “Process” block but that is not really required.  The Foreach loop allows the function to handle multiple paths passed from a Get-ChildItem.

The working steps

First, I skip any empty paths.  This is more of a precautionary step, except that somehow, they kept slipping through.  I set the $maxRetry variable to 5 as an endless loop prevention.  Testing didn’t require more than 2 iterations but I decided to add a few more to pad future improvements to the script.  The “takeown” executable I’m using require different switches for files and folders.  I build that command just before entering the Do loop.  Inside of the loop I execute the “takeown”, “icacls”, “attrib” , then chase them with the Remove-Item while capturing the output for them all.  At the end of the iteration I use regex to find any occurrences of “Access denied”, indicating that another iteration will be required.

Two more details worth noting include the use of “attrib” over setting the attributes directly with PowerShell and the attempt at deleting the files before repeating the loop.  Performance is the motivation for both.  Like the WMI example above, the performance wasn’t even close.  I tried a few pure PowerShell approaches before deciding it just doesn’t perform this task well in bulk.  Deleting the files just made logical sense.  The three applications I’m using in that loop aren’t terribly fast.  The first iteration takes care of the majority of the files so the additional loops are hardly noticeable.

# Sorry about the syntax highligher... Google's Prettify doesn't work so well for backticks in powershell

# Helper function to grant access to system folders
Function Remove-SystemFiles
{
    Param(
    # PassThru Path
    [Parameter(ValueFromPipeLineByPropertyName)]
    [Alias('FullName')]
    [string]$Paths
    )
    Process
    {
        Foreach($path in $Paths)
        {
            # Safety catch for am empty path
            If($path -eq $null){continue}
            # Need to cap this off to prevent a perpetual loop, 5 should be enough for most circumstances.
            $maxRetry = 5
            $isDirectory = ((Get-Item $path) -is [System.IO.DirectoryInfo])
            $takeOwnCmd = "TAKEOWN.exe /F $path /A"
            if($isDirectory)
            {
                $takeOwnCmd += " /R /D Y"
            }
            Do
            {
                $getPermissions = $takeOwnResult = $icaclsResult = $attribResult = $removeResult = $null
                # Force Owner
                Write-Verbose "Executing $takeOwnCmd"
                $takeOwnResult = Invoke-Expression $takeOwnCmd
                $takeOwnResult | Write-Verbose
                # Grant Permissions
                Write-Verbose "Executing icacls.exe `"$path`" /grant Administrators:F /inheritance:e /T /Q /C /L"
                $icaclsResult = icacls.exe "$path" /grant Administrators:F /inheritance:e /T /Q /C /L
                $takeOwnResult | Write-Verbose
                # Remove System Properties
                Write-Verbose 'Resetting Attributes.'
                Write-Verbose "Executing ATTRIB.exe -a `"$path`" /S /D /L"
                $attribResult = ATTRIB.exe -a `"$path`" /S /D /L
                # The pure powershell version works, but it's slow.
                # $attribResult = Get-ChildItem $oldWinPath -Recurse | ForEach-Object {
                # ($_ | Get-Item).Attributes = 'Normal'
                #}
                # Try to remove the folder
                Write-Verbose "Removing $path."
                try
                {
                    Remove-Item -Path $path -Force -Recurse -Verbose:$VerbosePreference -ea           SilentlyContinue -Confirm:$false | Write-Verbose
                }
                catch
                {
                    $removeResult = $_.Exception.Message
                }

                $getPermissions = ( $takeOwnResult + $icaclsResult + $attribResult + $removeResult) | Select-String -Pattern 'Access is denied'
            }
            While($getPermissions -ne $null -or (--$maxRetry -le 0))
            return $FilePath
         }
     }
 }

Wow….

That’s already a long post and I’ve only covered the Begin block.  The rest I’ll save for another post.  God Bless!

To Be Continued…

Part 2 (Coming Soon)

Joshua

See https://joshuaallenshaw.com/about-me/ See https://joshuaallenshaw.com/kiss/bio/