Public/Authoral/Invoke-SafeCleanup.ps1

<#PSScriptInfo

.VERSION 1.0

.GUID 8e69a94d-9fa1-4a5f-a23a-4546bdf5f39f

.AUTHOR Rodrigo Cordeiro

.PROJECTURI https://rodcordeiro.com.br/

.TAGS Windows Cleanup Temporary Cache

.RELEASENOTES
Initial safe cleanup function based on legacy BAT references.

#>


function Invoke-SafeCleanup {
<#
.SYNOPSIS
    Limpa arquivos temporarios e caches comuns do Windows com perfil conservador.

.DESCRIPTION
    Consolida as rotinas seguras observadas nos scripts BAT de referencia:
    temp de usuario, Windows Temp, caches de navegadores, INetCache e logs/caches
    temporarios. Por padrao remove apenas itens antigos e preserva as pastas-base.

    Use -Full para incluir uma limpeza mais completa, como lixeira, caches de
    Windows Update/Delivery Optimization, Adobe Media Cache, logs antigos e
    caches de todos os perfis locais quando executado como administrador.

    A funcao nao remove cookies, senhas, arquivos de sessao de navegador,
    Windows\Installer, MSOCache, dllcache, repair ou prefetch inteiro.

.PARAMETER Full
    Inclui alvos de maior impacto, ainda limitados a caches, logs e temporarios.

.PARAMETER MinimumAgeHours
    Idade minima dos itens removidos. O padrao evita remover arquivos recem-criados
    ou possivelmente em uso.

.PARAMETER PassThru
    Retorna um objeto com o resumo da limpeza.

.PARAMETER Silent
    Suprime mensagens informativas e resumo escritos no host.

.PARAMETER WhatIf
    Mostra o que seria removido sem apagar arquivos.

.EXAMPLE
    . .\Invoke-SafeCleanup.ps1
    Invoke-SafeCleanup

.EXAMPLE
    . .\Invoke-SafeCleanup.ps1
    Invoke-SafeCleanup -Full -WhatIf

.EXAMPLE
    . .\Invoke-SafeCleanup.ps1
    Invoke-SafeCleanup -Full -MinimumAgeHours 6 -PassThru

.EXAMPLE
    . .\Invoke-SafeCleanup.ps1
    Invoke-SafeCleanup -Silent
#>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [switch]$Full,

        [ValidateRange(0, 8760)]
        [int]$MinimumAgeHours = 24,

        [switch]$PassThru,

        [switch]$Silent
    )

    Set-StrictMode -Version Latest

    $isSilent = $Silent.IsPresent
    $cutoff = (Get-Date).AddHours(-$MinimumAgeHours)
    $summary = [ordered]@{
        Mode           = if ($Full) { 'Full' } else { 'Default' }
        MinimumAgeHours = $MinimumAgeHours
        TargetsVisited = 0
        FilesRemoved   = 0
        BytesRemoved   = 0L
        Skipped        = 0
        Errors         = 0
    }

    function Test-IsAdministrator {
        $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
        $principal = [Security.Principal.WindowsPrincipal]::new($identity)
        return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    }

    function Format-ByteSize {
        param([double]$Bytes)

        if ($Bytes -ge 1GB) { return '{0:N2} GB' -f ($Bytes / 1GB) }
        if ($Bytes -ge 1MB) { return '{0:N2} MB' -f ($Bytes / 1MB) }
        if ($Bytes -ge 1KB) { return '{0:N2} KB' -f ($Bytes / 1KB) }
        return '{0:N0} B' -f $Bytes
    }

    function Write-CleanupHost {
        param([AllowEmptyString()][string]$Message)

        if (-not $isSilent) {
            Write-Host $Message
        }
    }

    function Resolve-ExistingPath {
        param([Parameter(Mandatory)][string]$Path)

        try {
            return (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
        }
        catch {
            return $null
        }
    }

    function Test-SafeCleanupRoot {
        param([Parameter(Mandatory)][string]$Path)

        $resolved = Resolve-ExistingPath -Path $Path
        if (-not $resolved) { return $false }

        $blockedRoots = @(
            [Environment]::GetFolderPath('Windows'),
            [Environment]::GetFolderPath('ProgramFiles'),
            ${env:ProgramFiles(x86)},
            "$env:SystemDrive\",
            "$env:SystemDrive\Users"
        ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
            ForEach-Object { $_.TrimEnd('\') }

        $normalized = $resolved.TrimEnd('\')
        return -not ($blockedRoots -contains $normalized)
    }

    function Get-CleanupFiles {
        param(
            [Parameter(Mandatory)][string]$Path,
            [string[]]$Include = @('*')
        )

        if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
            return @()
        }

        foreach ($pattern in $Include) {
            Get-ChildItem -LiteralPath $Path -Force -Recurse -File -Include $pattern -ErrorAction SilentlyContinue |
                Where-Object { $_.LastWriteTime -lt $cutoff }
        }
    }

    function Remove-CleanupFiles {
        param(
            [Parameter(Mandatory)][string]$Label,
            [Parameter(Mandatory)][string]$Path,
            [string[]]$Include = @('*')
        )

        $resolved = Resolve-ExistingPath -Path $Path
        if (-not $resolved) {
            Write-Verbose "Ignorando '$Label': caminho inexistente ($Path)."
            return
        }

        if (-not (Test-SafeCleanupRoot -Path $resolved)) {
            Write-Warning "Ignorando '$Label': raiz bloqueada por seguranca ($resolved)."
            $summary.Skipped++
            return
        }

        $summary.TargetsVisited++
        $files = @(Get-CleanupFiles -Path $resolved -Include $Include)
        if ($files.Count -eq 0) {
            Write-CleanupHost "Sem itens antigos em $Label"
            return
        }

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

        $description = "$($files.Count) arquivo(s), $(Format-ByteSize -Bytes $bytes)"
        if ($PSCmdlet.ShouldProcess($resolved, "Remover $description de $Label")) {
            foreach ($file in $files) {
                try {
                    Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop
                    $summary.FilesRemoved++
                    $summary.BytesRemoved += $file.Length
                }
                catch {
                    $summary.Errors++
                    Write-Verbose "Nao foi possivel remover '$($file.FullName)': $($_.Exception.Message)"
                }
            }
        }
    }

    function Remove-EmptyDirectories {
        param(
            [Parameter(Mandatory)][string]$Label,
            [Parameter(Mandatory)][string]$Path
        )

        $resolved = Resolve-ExistingPath -Path $Path
        if (-not $resolved -or -not (Test-SafeCleanupRoot -Path $resolved)) {
            return
        }

        $directories = @(Get-ChildItem -LiteralPath $resolved -Force -Recurse -Directory -ErrorAction SilentlyContinue |
            Sort-Object -Property FullName -Descending)

        foreach ($directory in $directories) {
            try {
                $hasChildren = @(Get-ChildItem -LiteralPath $directory.FullName -Force -ErrorAction SilentlyContinue).Count -gt 0
                if (-not $hasChildren -and $PSCmdlet.ShouldProcess($directory.FullName, "Remover pasta vazia de $Label")) {
                    Remove-Item -LiteralPath $directory.FullName -Force -ErrorAction Stop
                }
            }
            catch {
                $summary.Errors++
                Write-Verbose "Nao foi possivel remover pasta '$($directory.FullName)': $($_.Exception.Message)"
            }
        }
    }

    function Get-LocalUserProfiles {
        param([switch]$AllUsers)

        if (-not $AllUsers) {
            return @([pscustomobject]@{
                Name = $env:USERNAME
                Path = $env:USERPROFILE
            })
        }

        Get-CimInstance -ClassName Win32_UserProfile -ErrorAction SilentlyContinue |
            Where-Object {
                $_.LocalPath -like "$env:SystemDrive\Users\*" -and
                -not $_.Special -and
                (Test-Path -LiteralPath $_.LocalPath -PathType Container)
            } |
            ForEach-Object {
                [pscustomobject]@{
                    Name = Split-Path -Path $_.LocalPath -Leaf
                    Path = $_.LocalPath
                }
            }
    }

    function Invoke-ProfileCleanup {
        param(
            [Parameter(Mandatory)]$Profile,
            [switch]$IncludeFullTargets
        )

        $base = $Profile.Path
        $name = $Profile.Name

        Remove-CleanupFiles -Label "Temp do usuario $name" -Path (Join-Path $base 'AppData\Local\Temp')
        Remove-EmptyDirectories -Label "Temp do usuario $name" -Path (Join-Path $base 'AppData\Local\Temp')

        Remove-CleanupFiles -Label "INetCache do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Windows\INetCache')
        Remove-CleanupFiles -Label "Cache principal do Edge do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Edge\User Data\Default\Cache\Cache_Data') -Include @('data*', 'f*', 'index', '*.tmp')
        Remove-CleanupFiles -Label "GPUCache do Edge do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Edge\User Data\Default\GPUCache') -Include @('d*', 'i*', '*.tmp')
        Remove-CleanupFiles -Label "Code Cache do Edge do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Edge\User Data\Default\Code Cache')
        Remove-CleanupFiles -Label "Service Worker Cache do Edge do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Edge\User Data\Default\Service Worker\CacheStorage')
        Remove-CleanupFiles -Label "Cache principal do Chrome do usuario $name" -Path (Join-Path $base 'AppData\Local\Google\Chrome\User Data\Default\Cache\Cache_Data') -Include @('data*', 'f*', 'index', '*.tmp')
        Remove-CleanupFiles -Label "GPUCache do Chrome do usuario $name" -Path (Join-Path $base 'AppData\Local\Google\Chrome\User Data\Default\GPUCache') -Include @('d*', 'i*', '*.tmp')
        Remove-CleanupFiles -Label "Code Cache do Chrome do usuario $name" -Path (Join-Path $base 'AppData\Local\Google\Chrome\User Data\Default\Code Cache')
        Remove-CleanupFiles -Label "Service Worker Cache do Chrome do usuario $name" -Path (Join-Path $base 'AppData\Local\Google\Chrome\User Data\Default\Service Worker\CacheStorage')
        Remove-CleanupFiles -Label "Cache do Firefox do usuario $name" -Path (Join-Path $base 'AppData\Local\Mozilla\Firefox\Profiles') -Include @('*.tmp', '*.log', 'startup*.*', 'script*.bin')
        Remove-CleanupFiles -Label "Cache do Opera do usuario $name" -Path (Join-Path $base 'AppData\Local\Opera Software')

        if ($IncludeFullTargets) {
            Remove-CleanupFiles -Label "WebCache logs do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Windows\WebCache') -Include @('*.log', '*.tmp')
            Remove-CleanupFiles -Label "Explorer thumb cache do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Windows\Explorer') -Include @('thumbcache*.db', '*.tmp')
            Remove-CleanupFiles -Label "Terminal Server Client cache do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Terminal Server Client\Cache') -Include @('*.bin')
            Remove-CleanupFiles -Label "Logs do OneDrive do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\OneDrive\setup\logs') -Include @('*.log')
            Remove-CleanupFiles -Label "Adobe Media Cache do usuario $name" -Path (Join-Path $base 'AppData\Roaming\Adobe\Common\Media Cache files')
            Remove-CleanupFiles -Label "Metricas do Edge do usuario $name" -Path (Join-Path $base 'AppData\Local\Microsoft\Edge\User Data\BrowserMetrics') -Include @('*.pma', '*.log')
            Remove-CleanupFiles -Label "Metricas do Chrome do usuario $name" -Path (Join-Path $base 'AppData\Local\Google\Chrome\User Data\BrowserMetrics') -Include @('*.pma', '*.log')
        }
    }

    Write-CleanupHost "Iniciando limpeza segura. Modo: $($summary.Mode). Itens mais novos que $MinimumAgeHours hora(s) serao preservados."

    $isAdmin = Test-IsAdministrator
    if ($Full -and -not $isAdmin) {
        Write-Warning 'Modo Full sem administrador: a limpeza completa ficara limitada ao perfil atual e caminhos acessiveis.'
    }

    $profiles = @(Get-LocalUserProfiles -AllUsers:($Full -and $isAdmin))
    foreach ($profile in $profiles) {
        Invoke-ProfileCleanup -Profile $profile -IncludeFullTargets:$Full
    }

    Remove-CleanupFiles -Label 'Windows Temp' -Path (Join-Path $env:WINDIR 'Temp')
    Remove-EmptyDirectories -Label 'Windows Temp' -Path (Join-Path $env:WINDIR 'Temp')

    if ($Full) {
        Remove-CleanupFiles -Label 'Logs CBS antigos' -Path (Join-Path $env:WINDIR 'Logs\CBS') -Include @('*.log')
        Remove-CleanupFiles -Label 'Logs MoSetup antigos' -Path (Join-Path $env:WINDIR 'Logs\MoSetup') -Include @('*.log')
        Remove-CleanupFiles -Label 'Logs Panther antigos' -Path (Join-Path $env:WINDIR 'Panther') -Include @('*.log')
        Remove-CleanupFiles -Label 'Logs INF antigos' -Path (Join-Path $env:WINDIR 'inf') -Include @('*.log')
        Remove-CleanupFiles -Label 'Logs SoftwareDistribution antigos' -Path (Join-Path $env:WINDIR 'SoftwareDistribution') -Include @('*.log')
        Remove-CleanupFiles -Label 'Cache de download do Windows Update' -Path (Join-Path $env:WINDIR 'SoftwareDistribution\Download')
        Remove-CleanupFiles -Label 'Cache de Delivery Optimization' -Path (Join-Path $env:ProgramData 'Microsoft\Windows\DeliveryOptimization\Cache')

        if ($PSCmdlet.ShouldProcess('Lixeira do usuario atual', 'Esvaziar lixeira')) {
            try {
                Clear-RecycleBin -Force -ErrorAction Stop
            }
            catch {
                $summary.Errors++
                Write-Verbose "Nao foi possivel esvaziar a lixeira: $($_.Exception.Message)"
            }
        }
    }

    $result = [pscustomobject]@{
        Mode           = $summary.Mode
        MinimumAgeHours = $summary.MinimumAgeHours
        TargetsVisited = $summary.TargetsVisited
        FilesRemoved   = $summary.FilesRemoved
        BytesRemoved   = $summary.BytesRemoved
        FreedSpace     = Format-ByteSize -Bytes $summary.BytesRemoved
        Skipped        = $summary.Skipped
        Errors         = $summary.Errors
    }

    Write-CleanupHost ''
    Write-CleanupHost 'Resumo da limpeza'
    Write-CleanupHost "Alvos avaliados : $($result.TargetsVisited)"
    Write-CleanupHost "Arquivos removidos: $($result.FilesRemoved)"
    Write-CleanupHost "Espaco liberado : $($result.FreedSpace)"
    Write-CleanupHost "Itens ignorados : $($result.Skipped)"
    Write-CleanupHost "Erros : $($result.Errors)"

    if ($PassThru) {
        return $result
    }
}

if ($ExecutionContext.SessionState.Module) {
    Export-ModuleMember -Function Invoke-SafeCleanup
}