1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 16:14:44 +00:00
Files
Mole/windows/lib/clean/apps.ps1
Bhadra 800db2429d fix(windows): address code review security and reliability issues
- Add protection checks to Go analyze tool before delete operations
- Use try/finally to ensure Windows Update service restarts after cleanup
- Don't count interactive uninstall as automatic success
- Tighten orphaned app detection with stricter prefix matching
2026-01-08 22:25:57 +05:30

443 lines
14 KiB
PowerShell

# Mole - Application-Specific Cleanup Module
# Cleans leftover data from uninstalled apps and app-specific caches
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_CLEAN_APPS_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_APPS_LOADED) { return }
$script:MOLE_CLEAN_APPS_LOADED = $true
# Import dependencies
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\..\core\base.ps1"
. "$scriptDir\..\core\log.ps1"
. "$scriptDir\..\core\file_ops.ps1"
# ============================================================================
# Orphaned App Data Detection
# ============================================================================
function Get-InstalledPrograms {
<#
.SYNOPSIS
Get list of installed programs from registry
#>
$programs = @()
$registryPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
foreach ($path in $registryPaths) {
$items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName } |
Select-Object DisplayName, InstallLocation, Publisher
if ($items) {
$programs += $items
}
}
# Also check UWP apps
try {
$uwpApps = Get-AppxPackage -ErrorAction SilentlyContinue |
Select-Object @{N='DisplayName';E={$_.Name}}, @{N='InstallLocation';E={$_.InstallLocation}}, Publisher
if ($uwpApps) {
$programs += $uwpApps
}
}
catch {
Write-Debug "Could not enumerate UWP apps: $_"
}
return $programs
}
function Find-OrphanedAppData {
<#
.SYNOPSIS
Find app data folders for apps that are no longer installed
#>
param([int]$DaysOld = 60)
$installedPrograms = Get-InstalledPrograms
$installedNames = $installedPrograms | ForEach-Object { $_.DisplayName.ToLower() }
$orphanedPaths = @()
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
# Check common app data locations
$appDataPaths = @(
@{ Path = $env:APPDATA; Type = "Roaming" }
@{ Path = $env:LOCALAPPDATA; Type = "Local" }
)
foreach ($location in $appDataPaths) {
if (-not (Test-Path $location.Path)) { continue }
$folders = Get-ChildItem -Path $location.Path -Directory -ErrorAction SilentlyContinue
foreach ($folder in $folders) {
# Skip system folders
$skipFolders = @('Microsoft', 'Windows', 'Packages', 'Programs', 'Temp', 'Roaming')
if ($folder.Name -in $skipFolders) { continue }
# Skip if recently modified
if ($folder.LastWriteTime -gt $cutoffDate) { continue }
# Check if app is installed using stricter matching
# Require exact match or that folder name is a clear prefix/suffix of app name
$isInstalled = $false
$folderLower = $folder.Name.ToLower()
foreach ($name in $installedNames) {
# Exact match
if ($name -eq $folderLower) {
$isInstalled = $true
break
}
# Folder is prefix of app name (e.g., "chrome" matches "chrome browser")
if ($name.StartsWith($folderLower) -and $folderLower.Length -ge 4) {
$isInstalled = $true
break
}
# App name is prefix of folder (e.g., "vscode" matches "vscode-data")
if ($folderLower.StartsWith($name) -and $name.Length -ge 4) {
$isInstalled = $true
break
}
}
if (-not $isInstalled) {
$orphanedPaths += @{
Path = $folder.FullName
Name = $folder.Name
Type = $location.Type
Size = (Get-PathSize -Path $folder.FullName)
LastModified = $folder.LastWriteTime
}
}
}
}
return $orphanedPaths
}
function Clear-OrphanedAppData {
<#
.SYNOPSIS
Clean orphaned application data
#>
param([int]$DaysOld = 60)
Start-Section "Orphaned app data"
$orphaned = Find-OrphanedAppData -DaysOld $DaysOld
if ($orphaned.Count -eq 0) {
Write-Info "No orphaned app data found"
Stop-Section
return
}
# Filter by size (only clean if > 10MB to avoid noise)
$significantOrphans = $orphaned | Where-Object { $_.Size -gt 10MB }
if ($significantOrphans.Count -gt 0) {
$totalSize = ($significantOrphans | Measure-Object -Property Size -Sum).Sum
$sizeHuman = Format-ByteSize -Bytes $totalSize
Write-Info "Found $($significantOrphans.Count) orphaned folders ($sizeHuman)"
foreach ($orphan in $significantOrphans) {
$orphanSize = Format-ByteSize -Bytes $orphan.Size
Remove-SafeItem -Path $orphan.Path -Description "$($orphan.Name) ($orphanSize)" -Recurse
}
}
Stop-Section
}
# ============================================================================
# Specific Application Cleanup
# ============================================================================
function Clear-OfficeCache {
<#
.SYNOPSIS
Clean Microsoft Office caches and temp files
#>
$officeCachePaths = @(
# Office 365 / 2019 / 2021
"$env:LOCALAPPDATA\Microsoft\Office\16.0\OfficeFileCache"
"$env:LOCALAPPDATA\Microsoft\Office\16.0\Wef"
"$env:LOCALAPPDATA\Microsoft\Outlook\RoamCache"
"$env:LOCALAPPDATA\Microsoft\Outlook\Offline Address Books"
# Older Office versions
"$env:LOCALAPPDATA\Microsoft\Office\15.0\OfficeFileCache"
# Office temp files
"$env:APPDATA\Microsoft\Templates\*.tmp"
"$env:APPDATA\Microsoft\Word\*.tmp"
"$env:APPDATA\Microsoft\Excel\*.tmp"
"$env:APPDATA\Microsoft\PowerPoint\*.tmp"
)
foreach ($path in $officeCachePaths) {
if ($path -like "*.tmp") {
$parent = Split-Path -Parent $path
if (Test-Path $parent) {
$tmpFiles = Get-ChildItem -Path $parent -Filter "*.tmp" -File -ErrorAction SilentlyContinue
if ($tmpFiles) {
$paths = $tmpFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Office temp files"
}
}
}
elseif (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Office $(Split-Path -Leaf $path)"
}
}
}
function Clear-OneDriveCache {
<#
.SYNOPSIS
Clean OneDrive cache
#>
$oneDriveCachePaths = @(
"$env:LOCALAPPDATA\Microsoft\OneDrive\logs"
"$env:LOCALAPPDATA\Microsoft\OneDrive\setup\logs"
)
foreach ($path in $oneDriveCachePaths) {
if (Test-Path $path) {
Remove-OldFiles -Path $path -DaysOld 7 -Description "OneDrive logs"
}
}
}
function Clear-DropboxCache {
<#
.SYNOPSIS
Clean Dropbox cache
#>
# Dropbox cache is typically in the Dropbox folder itself
$dropboxInfoPath = "$env:LOCALAPPDATA\Dropbox\info.json"
if (Test-Path $dropboxInfoPath) {
try {
$dropboxInfo = Get-Content $dropboxInfoPath | ConvertFrom-Json
$dropboxPath = $dropboxInfo.personal.path
if ($dropboxPath) {
$dropboxCachePath = "$dropboxPath\.dropbox.cache"
if (Test-Path $dropboxCachePath) {
Clear-DirectoryContents -Path $dropboxCachePath -Description "Dropbox cache"
}
}
}
catch {
Write-Debug "Could not read Dropbox config: $_"
}
}
}
function Clear-GoogleDriveCache {
<#
.SYNOPSIS
Clean Google Drive cache
#>
$googleDriveCachePaths = @(
"$env:LOCALAPPDATA\Google\DriveFS\Logs"
"$env:LOCALAPPDATA\Google\DriveFS\*.tmp"
)
foreach ($path in $googleDriveCachePaths) {
if ($path -like "*.tmp") {
$parent = Split-Path -Parent $path
if (Test-Path $parent) {
$tmpFiles = Get-ChildItem -Path $parent -Filter "*.tmp" -ErrorAction SilentlyContinue
if ($tmpFiles) {
$paths = $tmpFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Google Drive temp"
}
}
}
elseif (Test-Path $path) {
Remove-OldFiles -Path $path -DaysOld 7 -Description "Google Drive logs"
}
}
}
function Clear-AdobeData {
<#
.SYNOPSIS
Clean Adobe application caches and temp files
#>
$adobeCachePaths = @(
"$env:APPDATA\Adobe\Common\Media Cache Files"
"$env:APPDATA\Adobe\Common\Peak Files"
"$env:APPDATA\Adobe\Common\Team Projects Cache"
"$env:LOCALAPPDATA\Adobe\*\Cache"
"$env:LOCALAPPDATA\Adobe\*\CameraRaw\Cache"
"$env:LOCALAPPDATA\Temp\Adobe"
)
foreach ($pattern in $adobeCachePaths) {
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
foreach ($path in $paths) {
if (Test-Path $path.Path) {
Clear-DirectoryContents -Path $path.Path -Description "Adobe cache"
}
}
}
}
function Clear-AutodeskData {
<#
.SYNOPSIS
Clean Autodesk application caches
#>
$autodeskCachePaths = @(
"$env:LOCALAPPDATA\Autodesk\*\Cache"
"$env:APPDATA\Autodesk\*\cache"
)
foreach ($pattern in $autodeskCachePaths) {
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
foreach ($path in $paths) {
if (Test-Path $path.Path) {
Clear-DirectoryContents -Path $path.Path -Description "Autodesk cache"
}
}
}
}
# ============================================================================
# Gaming Platform Cleanup
# ============================================================================
function Clear-GamingPlatformCaches {
<#
.SYNOPSIS
Clean gaming platform caches (Steam, Epic, Origin, etc.)
#>
# Steam
$steamPaths = @(
"${env:ProgramFiles(x86)}\Steam\appcache\httpcache"
"${env:ProgramFiles(x86)}\Steam\appcache\librarycache"
"${env:ProgramFiles(x86)}\Steam\logs"
)
foreach ($path in $steamPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Steam $(Split-Path -Leaf $path)"
}
}
# Epic Games Launcher
$epicPaths = @(
"$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache"
"$env:LOCALAPPDATA\EpicGamesLauncher\Saved\Logs"
)
foreach ($path in $epicPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Epic Games $(Split-Path -Leaf $path)"
}
}
# EA App (Origin replacement)
$eaPaths = @(
"$env:LOCALAPPDATA\Electronic Arts\EA Desktop\cache"
"$env:APPDATA\Origin\*\cache"
)
foreach ($pattern in $eaPaths) {
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
foreach ($path in $paths) {
if (Test-Path $path.Path) {
Clear-DirectoryContents -Path $path.Path -Description "EA/Origin cache"
}
}
}
# GOG Galaxy
$gogPaths = @(
"$env:LOCALAPPDATA\GOG.com\Galaxy\webcache"
"$env:PROGRAMDATA\GOG.com\Galaxy\logs"
)
foreach ($path in $gogPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "GOG Galaxy $(Split-Path -Leaf $path)"
}
}
# Ubisoft Connect
$ubiPaths = @(
"$env:LOCALAPPDATA\Ubisoft Game Launcher\cache"
"$env:LOCALAPPDATA\Ubisoft Game Launcher\logs"
)
foreach ($path in $ubiPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Ubisoft $(Split-Path -Leaf $path)"
}
}
# Battle.net
$battlenetPaths = @(
"$env:APPDATA\Battle.net\Cache"
"$env:APPDATA\Battle.net\Logs"
)
foreach ($path in $battlenetPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Battle.net $(Split-Path -Leaf $path)"
}
}
}
# ============================================================================
# Main Application Cleanup Function
# ============================================================================
function Invoke-AppCleanup {
<#
.SYNOPSIS
Run all application-specific cleanup tasks
#>
param([switch]$IncludeOrphaned)
Start-Section "Applications"
# Productivity apps
Clear-OfficeCache
Clear-OneDriveCache
Clear-DropboxCache
Clear-GoogleDriveCache
# Creative apps
Clear-AdobeData
Clear-AutodeskData
# Gaming platforms
Clear-GamingPlatformCaches
Stop-Section
# Orphaned app data (separate section)
if ($IncludeOrphaned) {
Clear-OrphanedAppData -DaysOld 60
}
}
# ============================================================================
# Exports
# ============================================================================
# Functions: Get-InstalledPrograms, Find-OrphanedAppData, Clear-OfficeCache, etc.