Public/diskcleanup.ps1

#Requires -RunAsAdministrator

<#
.SYNOPSIS
    Comprehensive disk cleanup script for deployment
.DESCRIPTION
    Clears caches, temp files, GUID folders, and other unnecessary data for all user profiles
    Optionally removes old files from Downloads and Documents folders (1+ years old)
    Removes GUID-named temporary folders from user profiles
    Enhanced temp folder cleanup across all AppData locations
    Tracks space savings and provides detailed reporting
.NOTES
    Designed for - returns exit codes and structured output
    Configure $CleanOldDownloads and $FileAgeThresholdDays variables to control old file cleanup
    Protected file extensions (.exe, .msi, etc.) are preserved regardless of age
    GUID folders containing critical files (.exe, .dll, .sys, .msi) are preserved
    Author: Calvindd2f
    Version: 1.8
#>


$TotalSpaceFreed = 0
$CleanupReport = [System.Collections.Generic.List[object]]::new()
$ErrorLog = [System.Collections.Generic.List[string]]::new()

# Config
$CleanOldDownloads = $true # Set to $false to disable old file cleanup
$FileAgeThresholdDays = 365 # Files older than this will be deleted (1 year)
$ProtectedExtensions = @('.exe', '.msi', '.dmg', '.iso', '.vhd', '.vhdx', '.ova', '.ovf') # Files to preserve regardless of age

# Function to get folder size safely
function Get-FolderSize {
    param([string]$Path)
    try {
        if ([string]::IsNullOrEmpty($Path) -or -not (Test-Path $Path)) {
            return 0
        }

        $files = Get-ChildItem -Path $Path -Recurse -File -ErrorAction SilentlyContinue
        if ($null -eq $files) {
            return 0
        }

        $measurement = $files | Measure-Object -Property Length -Sum
        if ($null -eq $measurement -or $null -eq $measurement.Sum) {
            return 0
        }

        return [math]::Round($measurement.Sum / 1MB, 2)
    }
    catch {
        Write-Verbose "Error getting folder size for $Path : $($_.Exception.Message)"
        return 0
    }
}

# Function to clean directory safely
function Remove-DirectoryContents {
    param(
        [string]$Path,
        [string]$Description
    )

    try {
        if ([string]::IsNullOrEmpty($Path) -or -not (Test-Path $Path)) {
            return
        }

        $sizeBefore = Get-FolderSize -Path $Path

        # Remove files first, then empty directories - with better error handling
        $files = Get-ChildItem -Path $Path -Recurse -File -ErrorAction SilentlyContinue
        if ($null -ne $files) {
            foreach ($file in $files) {
                try {
                    if ($null -ne $file -and (Test-Path $file.FullName)) {
                        Remove-Item -Path $file.FullName -Force -ErrorAction SilentlyContinue
                    }
                }
                catch {
                    # Continue processing other files even if one fails
                    continue
                }
            }
        }

        # Remove empty directories
        $directories = Get-ChildItem -Path $Path -Recurse -Directory -ErrorAction SilentlyContinue
        if ($null -ne $directories) {
            $sortedDirs = $directories | Sort-Object FullName -Descending
            foreach ($dir in $sortedDirs) {
                try {
                    if ($null -ne $dir -and (Test-Path $dir.FullName)) {
                        Remove-Item -Path $dir.FullName -Force -Recurse -ErrorAction SilentlyContinue
                    }
                }
                catch {
                    # Continue processing other directories even if one fails
                    continue
                }
            }
        }

        $sizeAfter = Get-FolderSize -Path $Path
        $spaceFreed = $sizeBefore - $sizeAfter

        if ($spaceFreed -gt 0) {
            $script:TotalSpaceFreed += $spaceFreed
            $script:CleanupReport.Add([PSCustomObject]@{
                    Location   = $Description
                    Path       = $Path
                    SpaceFreed = "$spaceFreed MB"
                    Status     = "Success"
                })
            Write-Host "✓ $Description`: $spaceFreed MB freed" -ForegroundColor Green
        }
    }
    catch {
        $script:ErrorLog.Add("Error cleaning $Description ($Path): $($_.Exception.Message)")
        Write-Warning "Error cleaning $Description : $($_.Exception.Message)"
    }
}

# Function to clean browser caches for a user
function Clear-BrowserCaches {
    param([string]$UserProfile, [string]$Username)

    if ([string]::IsNullOrEmpty($UserProfile) -or [string]::IsNullOrEmpty($Username)) {
        return
    }

    $browserPaths = @{
        "Chrome Cache"  = "$UserProfile\AppData\Local\Google\Chrome\User Data\Default\Cache"
        "Chrome Cache2" = "$UserProfile\AppData\Local\Google\Chrome\User Data\Default\Cache2"
        "Edge Cache"    = "$UserProfile\AppData\Local\Microsoft\Edge\User Data\Default\Cache"
        "Firefox Cache" = "$UserProfile\AppData\Local\Mozilla\Firefox\Profiles\*\cache2"
        "IE Cache"      = "$UserProfile\AppData\Local\Microsoft\Windows\INetCache"
        "IE Cookies"    = "$UserProfile\AppData\Local\Microsoft\Windows\INetCookies"
    }

    foreach ($browser in $browserPaths.GetEnumerator()) {
        try {
            if ([string]::IsNullOrEmpty($browser.Value)) { continue }

            $paths = Get-ChildItem -Path $browser.Value -ErrorAction SilentlyContinue
            if ($null -ne $paths) {
                foreach ($path in $paths) {
                    if ($null -ne $path -and $null -ne $path.FullName) {
                        Remove-DirectoryContents -Path $path.FullName -Description "$($browser.Key) - $Username"
                    }
                }
            }
        }
        catch {
            $script:ErrorLog.Add("Error processing browser cache $($browser.Key) for $Username : $($_.Exception.Message)")
            continue
        }
    }
}

# Function to clean GUID-named folders from user profiles
function Remove-GuidFolders {
    param([string]$UserProfile, [string]$Username)

    if ([string]::IsNullOrEmpty($UserProfile) -or [string]::IsNullOrEmpty($Username)) {
        return
    }

    # GUID pattern: 8-4-4-4-12 hex digits
    $guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'

    # Common locations where GUID folders appear
    $guidLocations = @(
        "$UserProfile", # Root of user profile
        "$UserProfile\AppData\Local",
        "$UserProfile\AppData\Roaming",
        "$UserProfile\AppData\LocalLow"
    )

    Write-Host " Scanning for GUID-named folders..." -ForegroundColor Yellow
    $totalGuidFoldersRemoved = 0
    $totalGuidSpaceFreed = 0

    foreach ($location in $guidLocations) {
        try {
            if (-not (Test-Path $location)) { continue }

            $guidFolders = Get-ChildItem -Path $location -Directory -ErrorAction SilentlyContinue |
            Where-Object { $null -ne $_.Name -and $_.Name -match $guidPattern }

            if ($null -eq $guidFolders -or $guidFolders.Count -eq 0) { continue }

            foreach ($guidFolder in $guidFolders) {
                try {
                    if ($null -eq $guidFolder -or $null -eq $guidFolder.FullName) { continue }

                    $folderSize = Get-FolderSize -Path $guidFolder.FullName

                    # Skip if folder is in use or contains critical files
                    $criticalFiles = Get-ChildItem -Path $guidFolder.FullName -Recurse -File -ErrorAction SilentlyContinue |
                    Where-Object { $_.Extension -in @('.exe', '.dll', '.sys', '.msi') }

                    if ($null -ne $criticalFiles -and $criticalFiles.Count -gt 0) {
                        Write-Host " Skipping GUID folder with critical files: $($guidFolder.Name)" -ForegroundColor Gray
                        continue
                    }

                    Remove-Item -Path $guidFolder.FullName -Recurse -Force -ErrorAction Stop
                    $totalGuidFoldersRemoved++
                    $totalGuidSpaceFreed += $folderSize

                    Write-Verbose "Removed GUID folder: $($guidFolder.FullName) ($folderSize MB)"
                }
                catch {
                    $script:ErrorLog.Add("Error removing GUID folder $($guidFolder.FullName): $($_.Exception.Message)")
                    continue
                }
            }
        }
        catch {
            $script:ErrorLog.Add("Error scanning for GUID folders in $location : $($_.Exception.Message)")
            continue
        }
    }

    if ($totalGuidFoldersRemoved -gt 0) {
        $script:TotalSpaceFreed += $totalGuidSpaceFreed
        $script:CleanupReport.Add([PSCustomObject]@{
                Location   = "GUID Folders - $Username"
                Path       = $UserProfile
                SpaceFreed = "$totalGuidSpaceFreed MB"
                Status     = "Success - $totalGuidFoldersRemoved folders removed"
            })
        Write-Host " ✓ GUID Folders: $totalGuidFoldersRemoved folders removed, $totalGuidSpaceFreed MB freed" -ForegroundColor Green
    }
    else {
        Write-Host " No GUID folders found to remove" -ForegroundColor Gray
    }
}

function Remove-OldUserFiles {
    param([string]$UserProfile, [string]$Username)

    if (-not $CleanOldDownloads -or [string]::IsNullOrEmpty($UserProfile) -or [string]::IsNullOrEmpty($Username)) {
        if (-not $CleanOldDownloads) {
            Write-Host " Old file cleanup disabled - skipping Downloads/Documents" -ForegroundColor Gray
        }
        return
    }

    $cutoffDate = (Get-Date).AddDays(-$FileAgeThresholdDays)
    $foldersToClean = @{
        "Downloads" = "$UserProfile\Downloads"
        "Documents" = "$UserProfile\Documents"
    }

    foreach ($folder in $foldersToClean.GetEnumerator()) {
        try {
            $folderPath = $folder.Value
            if ([string]::IsNullOrEmpty($folderPath) -or -not (Test-Path $folderPath)) {
                continue
            }

            Write-Host " Scanning $($folder.Key) for files older than $FileAgeThresholdDays days..." -ForegroundColor Yellow

            $allFiles = Get-ChildItem -Path $folderPath -File -Recurse -ErrorAction SilentlyContinue
            if ($null -eq $allFiles) {
                Write-Host " No files found in $($folder.Key)" -ForegroundColor Gray
                continue
            }

            $oldFiles = [System.Collections.Generic.List[object]]::new()
            foreach ($file in $allFiles) {
                try {
                    if ($null -ne $file -and
                        $null -ne $file.LastWriteTime -and
                        $null -ne $file.Extension -and
                        $file.LastWriteTime -lt $cutoffDate -and
                        $file.Extension -notin $ProtectedExtensions) {
                        $oldFiles.Add($file)
                    }
                }
                catch {
                    # Skip files that cannot be accessed
                    continue
                }
            }

            if ($oldFiles.Count -eq 0) {
                Write-Host " No old files found in $($folder.Key)" -ForegroundColor Gray
                continue
            }

            $sizeBefore = 0
            foreach ($file in $oldFiles) {
                if ($null -ne $file -and $null -ne $file.Length) {
                    $sizeBefore += $file.Length
                }
            }
            $sizeBeforeMB = [math]::Round($sizeBefore / 1MB, 2)

            $deletedCount = 0
            foreach ($file in $oldFiles) {
                try {
                    if ($null -ne $file -and $null -ne $file.FullName -and (Test-Path $file.FullName)) {
                        Remove-Item -Path $file.FullName -Force -ErrorAction Stop
                        $deletedCount++
                    }
                }
                catch {
                    $script:ErrorLog.Add("Error deleting old file $($file.FullName): $($_.Exception.Message)")
                    continue
                }
            }

            if ($deletedCount -gt 0) {
                $script:TotalSpaceFreed += $sizeBeforeMB
                $script:CleanupReport.Add([PSCustomObject]@{
                        Location   = "$($folder.Key) Old Files - $Username"
                        Path       = $folderPath
                        SpaceFreed = "$sizeBeforeMB MB"
                        Status     = "Success - $deletedCount files deleted"
                    })
                Write-Host " ✓ $($folder.Key): $deletedCount old files deleted, $sizeBeforeMB MB freed" -ForegroundColor Green
            }
        }
        catch {
            $script:ErrorLog.Add("Error processing old files in $($folder.Key) for $Username : $($_.Exception.Message)")
            Write-Warning " Error processing old files in $($folder.Key): $($_.Exception.Message)"
        }
    }
}

Write-Host "=== Disk Cleanup Script ===" -ForegroundColor Cyan
Write-Host "Starting cleanup process..." -ForegroundColor Yellow

# Get all user profiles
try {
    $UserProfiles = Get-WmiObject -Class Win32_UserProfile |
    Where-Object {
        $null -ne $_ -and
        $_.Special -eq $false -and
        $null -ne $_.LocalPath -and
        $_.LocalPath -notlike "*\Windows\*" -and
        $_.LocalPath -ne ""
    }

    if ($null -eq $UserProfiles) {
        Write-Warning "No user profiles found to process"
        $UserProfiles = @()
    }

    Write-Host "Found $($UserProfiles.Count) user profiles to process" -ForegroundColor Yellow
}
catch {
    Write-Error "Failed to enumerate user profiles: $($_.Exception.Message)"
    exit 1
}

# Clean each user profile
foreach ($Profile in $UserProfiles) {
    try {
        if ($null -eq $Profile -or $null -eq $Profile.LocalPath) {
            continue
        }

        $UserPath = $Profile.LocalPath
        if ([string]::IsNullOrEmpty($UserPath) -or -not (Test-Path $UserPath)) {
            Write-Warning "Skipping invalid user profile path: $UserPath"
            continue
        }

        $Username = Split-Path $UserPath -Leaf
        if ([string]::IsNullOrEmpty($Username)) {
            Write-Warning "Skipping profile with empty username: $UserPath"
            continue
        }

        Write-Host "`nProcessing user: $Username" -ForegroundColor Cyan

        # User-specific temp and cache locations - Enhanced temp cleanup
        $userCleanupPaths = @{
            "User Temp - $Username"               = "$UserPath\AppData\Local\Temp"
            "User Temp2 - $Username"              = "$UserPath\AppData\Local\Tmp"
            "User Temp Roaming - $Username"       = "$UserPath\AppData\Roaming\Temp"
            "User Temp LocalLow - $Username"      = "$UserPath\AppData\LocalLow\Temp"
            "Recent Items - $Username"            = "$UserPath\AppData\Roaming\Microsoft\Windows\Recent"
            "Thumbnail Cache - $Username"         = "$UserPath\AppData\Local\Microsoft\Windows\Explorer"
            "Windows Error Reports - $Username"   = "$UserPath\AppData\Local\Microsoft\Windows\WER"
            "CrashDumps - $Username"              = "$UserPath\AppData\Local\CrashDumps"
            "Adobe Cache - $Username"             = "$UserPath\AppData\Local\Adobe\*\Cache"
            "Teams Cache - $Username"             = "$UserPath\AppData\Roaming\Microsoft\Teams\tmp"
            "Teams Cache2 - $Username"            = "$UserPath\AppData\Roaming\Microsoft\Teams\Cache"
            "Skype Cache - $Username"             = "$UserPath\AppData\Roaming\Skype\*\chatsync"
            "Discord Cache - $Username"           = "$UserPath\AppData\Roaming\discord\Cache"
            "Spotify Cache - $Username"           = "$UserPath\AppData\Local\Spotify\Storage"
            "Steam Logs - $Username"              = "$UserPath\AppData\Local\Steam\logs"
            "Windows Installer Cache - $Username" = "$UserPath\AppData\Local\Microsoft\Windows\INetCache\IE"
            "Office Cache - $Username"            = "$UserPath\AppData\Local\Microsoft\Office\*\OfficeFileCache"
            "OneDrive Temp - $Username"           = "$UserPath\AppData\Local\Microsoft\OneDrive\logs"
        }

        # Clean user-specific locations
        foreach ($location in $userCleanupPaths.GetEnumerator()) {
            try {
                if ([string]::IsNullOrEmpty($location.Value)) { continue }

                $paths = Get-ChildItem -Path $location.Value -ErrorAction SilentlyContinue
                if ($null -ne $paths) {
                    foreach ($path in $paths) {
                        if ($null -ne $path -and $null -ne $path.FullName) {
                            Remove-DirectoryContents -Path $path.FullName -Description $location.Key
                        }
                    }
                }
            }
            catch {
                $script:ErrorLog.Add("Error processing location $($location.Key): $($_.Exception.Message)")
                continue
            }
        }

        # Clean browser caches
        Clear-BrowserCaches -UserProfile $UserPath -Username $Username

        # Clean GUID-named folders
        Remove-GuidFolders -UserProfile $UserPath -Username $Username

        # Clean old files from Downloads and Documents
        Remove-OldUserFiles -UserProfile $UserPath -Username $Username

    }
    catch {
        $ErrorLog.Add("Error processing user $Username : $($_.Exception.Message)")
        Write-Warning "Error processing user $Username : $($_.Exception.Message)"
    }
}

Write-Host "`nCleaning system-wide locations..." -ForegroundColor Cyan

# System-wide cleanup locations
$systemCleanupPaths = @{
    "Windows Temp"          = "$env:WINDIR\Temp"
    "System Temp"           = "$env:TEMP"
    "System32 Temp"         = "$env:WINDIR\System32\config\systemprofile\AppData\Local\Temp"
    "Prefetch"              = "$env:WINDIR\Prefetch"
    "Software Distribution" = "$env:WINDIR\SoftwareDistribution\Download"
    "CBS Logs"              = "$env:WINDIR\Logs\CBS"
    "DISM Logs"             = "$env:WINDIR\Logs\DISM"
    "Windows Update Logs"   = "$env:WINDIR\WindowsUpdate.log*"
    "Memory Dumps"          = "$env:WINDIR\Minidump"
    "Error Reports"         = "$env:ALLUSERSPROFILE\Microsoft\Windows\WER"
    "Delivery Optimization" = "$env:WINDIR\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization\Logs"
    "IIS Logs"              = "$env:WINDIR\System32\LogFiles\W3SVC*"
    "Event Trace Logs"      = "$env:WINDIR\System32\LogFiles\WMI"
}

foreach ($location in $systemCleanupPaths.GetEnumerator()) {
    Remove-DirectoryContents -Path $location.Value -Description $location.Key
}

# Clean Recycle Bin
Write-Host "`nEmptying Recycle Bin..." -ForegroundColor Yellow
try {
    $sizeBefore = 0
    $recycleBins = Get-ChildItem -Path "C:\`$Recycle.Bin" -Directory -ErrorAction SilentlyContinue

    if ($null -ne $recycleBins) {
        foreach ($bin in $recycleBins) {
            if ($null -ne $bin -and $null -ne $bin.FullName) {
                $sizeBefore += Get-FolderSize -Path $bin.FullName
            }
        }
    }

    # Empty recycle bin using Shell.Application with better error handling
    $shell = $null
    $recycleBin = $null
    try {
        $shell = New-Object -ComObject Shell.Application
        if ($null -ne $shell) {
            $recycleBin = $shell.Namespace(0xA)
            if ($null -ne $recycleBin) {
                $items = $recycleBin.Items()
                if ($null -ne $items) {
                    foreach ($item in $items) {
                        try {
                            if ($null -ne $item) {
                                [void]$item.InvokeVerb("delete")
                            }
                        }
                        catch {
                            # Continue with other items if one fails
                            continue
                        }
                    }
                }
            }
        }
    }
    finally {
        # Clean up COM objects
        if ($null -ne $recycleBin) {
            [void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($recycleBin)
        }
        if ($null -ne $shell) {
            [void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell)
        }
    }

    if ($sizeBefore -gt 0) {
        $CleanupReport.Add([PSCustomObject]@{
                Location   = "Recycle Bin"
                Path       = "C:\`$Recycle.Bin"
                SpaceFreed = "$sizeBefore MB"
                Status     = "Success"
            })
        $TotalSpaceFreed += $sizeBefore
        Write-Host "✓ Recycle Bin: $sizeBefore MB freed" -ForegroundColor Green
    }
}
catch {
    $ErrorLog.Add("Error emptying Recycle Bin: $($_.Exception.Message)")
    Write-Warning "Error emptying Recycle Bin: $($_.Exception.Message)"
}

# Run Disk Cleanup utility for additional cleaning
Write-Host "`nRunning Windows Disk Cleanup..." -ForegroundColor Yellow
try {
    $cleanupBefore = (Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceID='C:'").FreeSpace
    
    # Check if cleanmgr.exe exists and is accessible
    if (Test-Path "C:\Windows\System32\cleanmgr.exe") {
        Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:1" -Wait -WindowStyle Hidden -ErrorAction SilentlyContinue
        Start-Sleep -Seconds 5
        $cleanupAfter = (Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceID='C:'").FreeSpace
        $cleanupFreed = [math]::Round(($cleanupAfter - $cleanupBefore) / 1MB, 2)

        if ($cleanupFreed -gt 0) {
            $TotalSpaceFreed += $cleanupFreed
            $CleanupReport.Add([PSCustomObject]@{
                    Location   = "Windows Disk Cleanup"
                    Path       = "System Utility"
                    SpaceFreed = "$cleanupFreed MB"
                    Status     = "Success"
                })
            Write-Host "✓ Windows Disk Cleanup: $cleanupFreed MB freed" -ForegroundColor Green
        }
    } else {
        Write-Warning "cleanmgr.exe not found or not accessible"
    }
}
catch {
    $ErrorLog.Add("Error running Disk Cleanup: $($_.Exception.Message)")
    Write-Warning "Error running Disk Cleanup: $($_.Exception.Message)"
}

# Output
Write-Host "`n=== CLEANUP SUMMARY ===" -ForegroundColor Green
Write-Host "Total Space Freed: $([math]::Round($TotalSpaceFreed, 2)) MB ($([math]::Round($TotalSpaceFreed / 1024, 2)) GB)" -ForegroundColor Green
Write-Host "Locations Cleaned: $($CleanupReport.Count)" -ForegroundColor Green
Write-Host "Errors Encountered: $($ErrorLog.Count)" -ForegroundColor $(if ($ErrorLog.Count -eq 0) { 'Green' }else { 'Red' })

# Display detailed report if requested or if there were errors
if ($ErrorLog.Count -gt 0) {
    Write-Host "`n=== ERRORS ===" -ForegroundColor Red
    $ErrorLog | ForEach-Object { Write-Host $_ -ForegroundColor Red }
}

# monitoring
$OutputData = @{
    TotalSpaceFreedMB = [math]::Round($TotalSpaceFreed, 2)
    TotalSpaceFreedGB = [math]::Round($TotalSpaceFreed / 1024, 2)
    LocationsCleaned  = $CleanupReport.Count
    ErrorCount        = $ErrorLog.Count
    Status            = if ($ErrorLog.Count -eq 0) { "Success" }else { "Warning" }
}

Write-Host "`nOUTPUT: $($OutputData | ConvertTo-Json -Compress)" -ForegroundColor Magenta

# Set appropriate exit code for
if ($ErrorLog.Count -eq 0) {
    Write-Host "`nCleanup completed successfully!" -ForegroundColor Green
    exit 0
}
else {
    Write-Host "`nCleanup completed with warnings. Check error log." -ForegroundColor Yellow
    exit 1
}