Public/windowsupdate/Clear-WindowsUpdateCache.ps1

#Requires -Version 5.1
function Clear-WindowsUpdateCache {
    <#
        .SYNOPSIS
            Clears the Windows Update download cache to free disk space
 
        .DESCRIPTION
            Stops the Windows Update (wuaserv) and BITS services, removes all files
            from the SoftwareDistribution\Download folder, then restarts both services.
            Reports the amount of disk space freed.
            This is useful when the cache becomes corrupted, takes up excessive space,
            or when troubleshooting Windows Update failures. The cache is automatically
            rebuilt on the next update scan.
 
        .PARAMETER ComputerName
            One or more computer names to target. Defaults to the local computer.
            Accepts pipeline input by value and by property name.
 
        .PARAMETER Credential
            Optional PSCredential for authenticating to remote computers.
            Not required for local operations.
 
        .PARAMETER IncludeDataStore
            When specified, also clears the DataStore folder which contains the
            Windows Update database. This forces a full resync with the update
            source. Use with caution.
 
        .EXAMPLE
            Clear-WindowsUpdateCache
 
            Clears the download cache on the local computer.
 
        .EXAMPLE
            Clear-WindowsUpdateCache -ComputerName 'SRV01' -IncludeDataStore
 
            Clears both the download cache and the DataStore on SRV01.
 
        .EXAMPLE
            'SRV01', 'SRV02' | Clear-WindowsUpdateCache
 
            Clears the download cache on SRV01 and SRV02 via pipeline.
 
        .OUTPUTS
            PSWinOps.WindowsUpdateCacheResult
            Returns objects with ComputerName, CachePath, FileCount, SizeFreedMB,
            DataStoreCleared, Result, and Timestamp properties.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-04-08
            Requires: PowerShell 5.1+ / Windows only
            Requires: Administrator privileges (to stop services and delete cache)
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/troubleshoot/windows-client/installing-updates-features-roles/additional-resources-for-windows-update
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType('PSWinOps.WindowsUpdateCacheResult')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter(Mandatory = $false)]
        [switch]$IncludeDataStore
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        $clearScriptBlock = {
            param(
                [bool]$ClearDataStore
            )

            $basePath = Join-Path -Path $env:SystemRoot -ChildPath 'SoftwareDistribution'
            $downloadPath = Join-Path -Path $basePath -ChildPath 'Download'
            $dataStorePath = Join-Path -Path $basePath -ChildPath 'DataStore'

            # Measure cache size before clearing
            $totalSize = 0
            $totalFiles = 0

            if (Test-Path -Path $downloadPath -PathType Container) {
                $items = Get-ChildItem -Path $downloadPath -Recurse -Force -ErrorAction SilentlyContinue
                $totalFiles = @($items | Where-Object -FilterScript { -not $_.PSIsContainer }).Count
                $totalSize = ($items | Measure-Object -Property 'Length' -Sum -ErrorAction SilentlyContinue).Sum
                if ($null -eq $totalSize) {
                    $totalSize = 0
                }
            }

            if ($ClearDataStore -and (Test-Path -Path $dataStorePath -PathType Container)) {
                $dsItems = Get-ChildItem -Path $dataStorePath -Recurse -Force -ErrorAction SilentlyContinue
                $dsFileCount = @($dsItems | Where-Object -FilterScript { -not $_.PSIsContainer }).Count
                $dsSize = ($dsItems | Measure-Object -Property 'Length' -Sum -ErrorAction SilentlyContinue).Sum
                if ($null -eq $dsSize) {
                    $dsSize = 0
                }
                $totalFiles += $dsFileCount
                $totalSize += $dsSize
            }

            # Stop services
            $servicesToStop = @('wuauserv', 'bits', 'cryptsvc')
            $stoppedServices = [System.Collections.Generic.List[string]]::new()

            foreach ($svcName in $servicesToStop) {
                try {
                    $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
                    if ($svc -and $svc.Status -eq 'Running') {
                        Stop-Service -Name $svcName -Force -ErrorAction Stop
                        $stoppedServices.Add($svcName)
                    }
                } catch {
                    Write-Warning "Could not stop service '$svcName': $_"
                }
            }

            # Wait for services to fully stop
            Start-Sleep -Seconds 2

            # Clear download cache
            $errors = [System.Collections.Generic.List[string]]::new()

            if (Test-Path -Path $downloadPath -PathType Container) {
                try {
                    Get-ChildItem -Path $downloadPath -Force -ErrorAction Stop |
                        Remove-Item -Recurse -Force -ErrorAction Stop
                } catch {
                    $errors.Add("Download folder: $_")
                }
            }

            # Clear DataStore if requested
            if ($ClearDataStore -and (Test-Path -Path $dataStorePath -PathType Container)) {
                try {
                    Get-ChildItem -Path $dataStorePath -Force -ErrorAction Stop |
                        Remove-Item -Recurse -Force -ErrorAction Stop
                } catch {
                    $errors.Add("DataStore folder: $_")
                }
            }

            # Restart services
            foreach ($svcName in $stoppedServices) {
                try {
                    Start-Service -Name $svcName -ErrorAction Stop
                } catch {
                    $errors.Add("Failed to restart service '$svcName': $_")
                }
            }

            $result = if ($errors.Count -eq 0) {
                'Succeeded'
            } else {
                'PartialSuccess'
            }

            return [PSCustomObject]@{
                CachePath        = $downloadPath
                FileCount        = $totalFiles
                SizeBytes        = $totalSize
                DataStoreCleared = $ClearDataStore
                Result           = $result
                Errors           = @($errors)
            }
        }
    }

    process {
        foreach ($computer in $ComputerName) {
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Processing '$computer'"

            $targetDesc = if ($IncludeDataStore) {
                "Clear Windows Update cache + DataStore on '$computer'"
            } else {
                "Clear Windows Update download cache on '$computer'"
            }

            if (-not $PSCmdlet.ShouldProcess($computer, $targetDesc)) {
                continue
            }

            try {
                $invokeParams = @{
                    ComputerName = $computer
                    ScriptBlock  = $clearScriptBlock
                    ArgumentList = @([bool]$IncludeDataStore)
                }
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    $invokeParams['Credential'] = $Credential
                }

                $clearResult = Invoke-RemoteOrLocal @invokeParams

                $sizeFreedMB = [math]::Round($clearResult.SizeBytes / 1MB, 2)

                if ($clearResult.Errors.Count -gt 0) {
                    foreach ($err in $clearResult.Errors) {
                        Write-Warning -Message "[$($MyInvocation.MyCommand)] '$computer' — $err"
                    }
                }

                Write-Verbose -Message "[$($MyInvocation.MyCommand)] '$computer' — Freed $sizeFreedMB MB ($($clearResult.FileCount) files)"

                [PSCustomObject]@{
                    PSTypeName       = 'PSWinOps.WindowsUpdateCacheResult'
                    ComputerName     = $computer
                    CachePath        = $clearResult.CachePath
                    FileCount        = $clearResult.FileCount
                    SizeFreedMB      = $sizeFreedMB
                    DataStoreCleared = $clearResult.DataStoreCleared
                    Result           = $clearResult.Result
                    Timestamp        = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                }
            } catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${computer}': $_"
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}