Загрузка данных


$ErrorActionPreference = "Continue"

$TimeStamp = Get-Date -Format "yyyyMMdd_HHmmss"
$Log = "C:\Disk_C_AllUsers_Cache_Cleanup_$TimeStamp.log"

function Write-Log {
    param(
        [string]$Text
    )

    $Line = "{0}  {1}" -f `
        (Get-Date -Format "yyyy-MM-dd HH:mm:ss"),
        $Text

    Write-Host $Line

    $Line |
        Out-File `
            -FilePath $Log `
            -Append `
            -Encoding UTF8
}

function Format-Size {
    param(
        [long]$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} B" -f $Bytes
}

function Get-CDriveInfo {

    $Drive = Get-WmiObject `
        -Class Win32_LogicalDisk `
        -Filter "DeviceID='C:'"

    [pscustomobject]@{
        SizeGB = [math]::Round(
            $Drive.Size / 1GB,
            2
        )

        UsedGB = [math]::Round(
            ($Drive.Size - $Drive.FreeSpace) / 1GB,
            2
        )

        FreeGB = [math]::Round(
            $Drive.FreeSpace / 1GB,
            2
        )

        FreePct = [math]::Round(
            ($Drive.FreeSpace / $Drive.Size) * 100,
            1
        )
    }
}

function Get-PathSize {
    param(
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        return 0L
    }

    try {

        $Item = Get-Item `
            -LiteralPath $Path `
            -Force `
            -ErrorAction Stop

        if (-not $Item.PSIsContainer) {
            return [long]$Item.Length
        }

        $Measure = Get-ChildItem `
            -LiteralPath $Path `
            -File `
            -Force `
            -Recurse `
            -ErrorAction SilentlyContinue |
            Measure-Object `
                -Property Length `
                -Sum

        if ($Measure.Sum) {
            return [long]$Measure.Sum
        }
    }
    catch {
    }

    return 0L
}

function Get-ProfileUserName {
    param(
        [string]$SID,
        [string]$ProfilePath
    )

    try {

        $SecurityIdentifier = New-Object `
            System.Security.Principal.SecurityIdentifier(
                $SID
            )

        $Account = $SecurityIdentifier.Translate(
            [System.Security.Principal.NTAccount]
        ).Value

        return ($Account -split "\\")[-1]
    }
    catch {

        return Split-Path `
            -Path $ProfilePath `
            -Leaf
    }
}

function Get-BlockingProcesses {
    param(
        [string]$UserName,
        [string[]]$ProcessNames
    )

    if (-not $ProcessNames -or $ProcessNames.Count -eq 0) {
        return @()
    }

    return @(
        $Script:OwnedProcesses |
            Where-Object {
                $_.User -ieq $UserName -and
                $ProcessNames -icontains $_.Name
            }
    )
}

function Clear-CacheContents {
    param(
        [string]$ProfilePath,
        [string]$UserName,
        [string]$Category,
        [string]$Path,
        [string[]]$BlockingProcesses
    )

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

    $SizeBefore = Get-PathSize -Path $Path

    if ($SizeBefore -eq 0) {
        return
    }

    $Blockers = Get-BlockingProcesses `
        -UserName $UserName `
        -ProcessNames $BlockingProcesses

    if ($Blockers.Count -gt 0) {

        $Names = (
            $Blockers |
                Select-Object `
                    -ExpandProperty Name `
                    -Unique
        ) -join ", "

        Write-Log (
            "ПРОПУЩЕНО: {0}; пользователь: {1}; процессы: {2}; путь: {3}" -f `
                $Category,
                $UserName,
                $Names,
                $Path
        )

        $Script:SkippedCount++
        return
    }

    Write-Log (
        "Очистка: {0}; пользователь: {1}; размер: {2}; путь: {3}" -f `
            $Category,
            $UserName,
            (Format-Size $SizeBefore),
            $Path
    )

    $ErrorsBefore = $Script:ErrorCount

    $Items = Get-ChildItem `
        -LiteralPath $Path `
        -Force `
        -ErrorAction SilentlyContinue

    foreach ($Item in $Items) {

        try {

            Remove-Item `
                -LiteralPath $Item.FullName `
                -Recurse `
                -Force `
                -ErrorAction Stop
        }
        catch {

            $Script:ErrorCount++

            Write-Log (
                "Не удалось удалить: {0}; ошибка: {1}" -f `
                    $Item.FullName,
                    $_.Exception.Message
            )
        }
    }

    $SizeAfter = Get-PathSize -Path $Path
    $Freed = $SizeBefore - $SizeAfter

    if ($Freed -lt 0) {
        $Freed = 0
    }

    $Script:RemovedBytes += $Freed

    Write-Log (
        "Результат: {0}; освобождено: {1}; осталось: {2}; ошибок: {3}" -f `
            $Category,
            (Format-Size $Freed),
            (Format-Size $SizeAfter),
            ($Script:ErrorCount - $ErrorsBefore)
    )
}

function Clear-OldFiles {
    param(
        [string]$ProfilePath,
        [string]$UserName,
        [string]$Category,
        [string]$Path,
        [int]$OlderThanDays
    )

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

    $Limit = (Get-Date).AddDays(-$OlderThanDays)

    $Files = @(
        Get-ChildItem `
            -LiteralPath $Path `
            -File `
            -Force `
            -Recurse `
            -ErrorAction SilentlyContinue |
            Where-Object {
                $_.LastWriteTime -lt $Limit
            }
    )

    if ($Files.Count -eq 0) {
        return
    }

    $SizeBefore = (
        $Files |
            Measure-Object `
                -Property Length `
                -Sum
    ).Sum

    if (-not $SizeBefore) {
        $SizeBefore = 0L
    }

    Write-Log (
        "Очистка: {0}; пользователь: {1}; старше: {2} дней; размер: {3}" -f `
            $Category,
            $UserName,
            $OlderThanDays,
            (Format-Size $SizeBefore)
    )

    $Removed = 0L

    foreach ($File in $Files) {

        try {

            $Length = [long]$File.Length

            Remove-Item `
                -LiteralPath $File.FullName `
                -Force `
                -ErrorAction Stop

            $Removed += $Length
        }
        catch {

            $Script:ErrorCount++

            Write-Log (
                "Не удалось удалить: {0}; ошибка: {1}" -f `
                    $File.FullName,
                    $_.Exception.Message
            )
        }
    }

    Get-ChildItem `
        -LiteralPath $Path `
        -Directory `
        -Force `
        -Recurse `
        -ErrorAction SilentlyContinue |
        Sort-Object `
            -Property FullName `
            -Descending |
        ForEach-Object {

            try {

                $Children = Get-ChildItem `
                    -LiteralPath $_.FullName `
                    -Force `
                    -ErrorAction SilentlyContinue

                if (-not $Children) {

                    Remove-Item `
                        -LiteralPath $_.FullName `
                        -Force `
                        -ErrorAction SilentlyContinue
                }
            }
            catch {
            }
        }

    $Script:RemovedBytes += $Removed

    Write-Log (
        "Результат: {0}; освобождено: {1}" -f `
            $Category,
            (Format-Size $Removed)
    )
}

function Clear-FirefoxCaches {
    param(
        [string]$ProfilePath,
        [string]$UserName
    )

    $FirefoxProcesses = @(
        "firefox.exe"
    )

    $Blockers = Get-BlockingProcesses `
        -UserName $UserName `
        -ProcessNames $FirefoxProcesses

    if ($Blockers.Count -gt 0) {

        Write-Log (
            "Firefox-кэш пропущен для {0}: Firefox запущен" -f `
                $UserName
        )

        $Script:SkippedCount++
        return
    }

    $FirefoxRoot = Join-Path `
        $ProfilePath `
        "AppData\Local\Mozilla\Firefox\Profiles"

    if (-not (Test-Path -LiteralPath $FirefoxRoot)) {
        return
    }

    $FirefoxProfiles = Get-ChildItem `
        -LiteralPath $FirefoxRoot `
        -Directory `
        -Force `
        -ErrorAction SilentlyContinue

    foreach ($FirefoxProfile in $FirefoxProfiles) {

        foreach ($CacheName in @(
            "cache2",
            "startupCache",
            "shader-cache",
            "OfflineCache",
            "thumbnails"
        )) {

            $CachePath = Join-Path `
                $FirefoxProfile.FullName `
                $CacheName

            Clear-CacheContents `
                -ProfilePath $ProfilePath `
                -UserName $UserName `
                -Category ("Firefox {0}" -f $CacheName) `
                -Path $CachePath `
                -BlockingProcesses @()
        }
    }
}

function Clear-ChromiumCaches {
    param(
        [string]$ProfilePath,
        [string]$UserName
    )

    $ChromiumProcesses = @(
        "chrome.exe",
        "msedge.exe",
        "browser.exe",
        "yandex.exe",
        "opera.exe"
    )

    $Blockers = Get-BlockingProcesses `
        -UserName $UserName `
        -ProcessNames $ChromiumProcesses

    if ($Blockers.Count -gt 0) {

        $Names = (
            $Blockers |
                Select-Object `
                    -ExpandProperty Name `
                    -Unique
        ) -join ", "

        Write-Log (
            "Chromium-кэши пропущены для {0}: запущены процессы {1}" -f `
                $UserName,
                $Names
        )

        $Script:SkippedCount++
        return
    }

    $BrowserRoots = @(
        (Join-Path $ProfilePath "AppData\Local\Google\Chrome\User Data"),
        (Join-Path $ProfilePath "AppData\Local\Microsoft\Edge\User Data"),
        (Join-Path $ProfilePath "AppData\Local\Yandex\YandexBrowser\User Data")
    )

    foreach ($BrowserRoot in $BrowserRoots) {

        if (-not (Test-Path -LiteralPath $BrowserRoot)) {
            continue
        }

        $BrowserProfiles = Get-ChildItem `
            -LiteralPath $BrowserRoot `
            -Directory `
            -Force `
            -ErrorAction SilentlyContinue |
            Where-Object {
                $_.Name -eq "Default" -or
                $_.Name -like "Profile *"
            }

        foreach ($BrowserProfile in $BrowserProfiles) {

            foreach ($CacheName in @(
                "Cache",
                "Code Cache",
                "GPUCache",
                "DawnCache",
                "GrShaderCache",
                "ShaderCache"
            )) {

                $CachePath = Join-Path `
                    $BrowserProfile.FullName `
                    $CacheName

                Clear-CacheContents `
                    -ProfilePath $ProfilePath `
                    -UserName $UserName `
                    -Category ("Chromium {0}" -f $CacheName) `
                    -Path $CachePath `
                    -BlockingProcesses @()
            }
        }
    }
}

$IsAdmin = (
    New-Object Security.Principal.WindowsPrincipal(
        [Security.Principal.WindowsIdentity]::GetCurrent()
    )
).IsInRole(
    [Security.Principal.WindowsBuiltInRole]::Administrator
)

if (-not $IsAdmin) {

    Write-Host ""
    Write-Host "PowerShell необходимо запустить от имени администратора." `
        -ForegroundColor Red
    Write-Host ""

    return
}

$Before = Get-CDriveInfo

$Script:RemovedBytes = 0L
$Script:ErrorCount = 0
$Script:SkippedCount = 0

Write-Log "============================================================"
Write-Log "ОЧИСТКА КЭШЕЙ ВСЕХ ПОЛЬЗОВАТЕЛЕЙ"
Write-Log "============================================================"

Write-Log (
    "До очистки: свободно {0} GB из {1} GB, {2}%" -f `
        $Before.FreeGB,
        $Before.SizeGB,
        $Before.FreePct
)

Write-Log ""
Write-Log "Получение процессов и их владельцев..."

$Script:OwnedProcesses = foreach (
    $Process in Get-WmiObject -Class Win32_Process
) {

    try {

        $Owner = $Process.GetOwner()

        if ($Owner.ReturnValue -eq 0) {

            [pscustomobject]@{
                User = $Owner.User
                Domain = $Owner.Domain
                Name = $Process.Name
                ProcessId = $Process.ProcessId
            }
        }
    }
    catch {
    }
}

Write-Log (
    "Получено процессов: {0}" -f `
        @($Script:OwnedProcesses).Count
)

$Profiles = Get-WmiObject `
    -Class Win32_UserProfile |
    Where-Object {
        -not $_.Special -and
        $_.LocalPath -like "C:\Users\*" -and
        (Test-Path -LiteralPath $_.LocalPath)
    } |
    Sort-Object `
        -Property LocalPath

Write-Log (
    "Найдено пользовательских профилей: {0}" -f `
        @($Profiles).Count
)

$PythonRustProcesses = @(
    "python.exe",
    "pythonw.exe",
    "pip.exe",
    "pip3.exe",
    "uv.exe",
    "cargo.exe",
    "rustc.exe",
    "rustup.exe",
    "rustup-init.exe",
    "maturin.exe"
)

$NodeProcesses = @(
    "node.exe",
    "npm.exe",
    "npm.cmd",
    "npx.exe",
    "npx.cmd"
)

$JavaProcesses = @(
    "java.exe",
    "javaw.exe",
    "soapui.exe"
)

$RdpProcesses = @(
    "mstsc.exe"
)

foreach ($Profile in $Profiles) {

    $ProfilePath = $Profile.LocalPath

    $UserName = Get-ProfileUserName `
        -SID $Profile.SID `
        -ProfilePath $ProfilePath

    Write-Log ""
    Write-Log "------------------------------------------------------------"

    Write-Log (
        "Профиль: {0}; пользователь: {1}; загружен: {2}" -f `
            $ProfilePath,
            $UserName,
            $Profile.Loaded
    )

    Clear-OldFiles `
        -ProfilePath $ProfilePath `
        -UserName $UserName `
        -Category "TEMP" `
        -Path (Join-Path $ProfilePath "AppData\Local\Temp") `
        -OlderThanDays 14

    Clear-OldFiles `
        -ProfilePath $ProfilePath `
        -UserName $UserName `
        -Category "CrashDumps" `
        -Path (Join-Path $ProfilePath "AppData\Local\CrashDumps") `
        -OlderThanDays 7

    Clear-CacheContents `
        -ProfilePath $ProfilePath `
        -UserName $UserName `
        -Category "puccinialin" `
        -Path (Join-Path $ProfilePath "AppData\Local\puccinialin") `
        -BlockingProcesses $PythonRustProcesses

    Clear-CacheContents `
        -ProfilePath $ProfilePath `
        -UserName $UserName `
        -Category "pip cache" `
        -Path (Join-Path $ProfilePath "AppData\Local\pip\cache") `
        -BlockingProcesses $PythonRustProcesses

    Clear-CacheContents `
        -ProfilePath $ProfilePath `
        -UserName $UserName `
        -Category "npm cache" `
        -Path (Join-Path $ProfilePath "AppData\Local\npm-cache") `
        -BlockingProcesses $NodeProcesses

    Clear-CacheContents `
        -ProfilePath $ProfilePath `
        -UserName $UserName `
        -Category "OpenJFX cache" `
        -Path (Join-Path $ProfilePath ".openjfx\cache") `
        -BlockingProcesses $JavaProcesses

    Clear-CacheContents `
        -ProfilePath $ProfilePath `
        -UserName $UserName `
        -Category "RDP bitmap cache" `
        -Path (
            Join-Path `
                $ProfilePath `
                "AppData\Local\Microsoft\Terminal Server Client\Cache"
        ) `
        -BlockingProcesses $RdpProcesses

    Clear-FirefoxCaches `
        -ProfilePath $ProfilePath `
        -UserName $UserName

    Clear-ChromiumCaches `
        -ProfilePath $ProfilePath `
        -UserName $UserName
}

$After = Get-CDriveInfo

$FreedOnDisk = [math]::Round(
    $After.FreeGB - $Before.FreeGB,
    2
)

Write-Log ""
Write-Log "============================================================"
Write-Log "ОЧИСТКА ЗАВЕРШЕНА"
Write-Log "============================================================"

Write-Log (
    "До очистки свободно: {0} GB" -f `
        $Before.FreeGB
)

Write-Log (
    "После очистки свободно: {0} GB" -f `
        $After.FreeGB
)

Write-Log (
    "Освобождено по диску: {0} GB" -f `
        $FreedOnDisk
)

Write-Log (
    "Удалено файлов кэша: {0}" -f `
        (Format-Size $Script:RemovedBytes)
)

Write-Log (
    "Пропущено операций из-за работающих приложений: {0}" -f `
        $Script:SkippedCount
)

Write-Log (
    "Ошибок удаления: {0}" -f `
        $Script:ErrorCount
)

Write-Log (
    "Свободно сейчас: {0}%" -f `
        $After.FreePct
)

Write-Log (
    "Лог: {0}" -f `
        $Log
)

Write-Host ""
Write-Host "============================================================"
Write-Host "СОСТОЯНИЕ ДИСКА C:"
Write-Host "============================================================"
Write-Host ""

Get-CDriveInfo |
    Format-List

Write-Host "Лог: $Log" -ForegroundColor Cyan