mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 18:34:46 +00:00
440 lines
12 KiB
PowerShell
440 lines
12 KiB
PowerShell
# Mole - Safe File Operations Module
|
|
# Provides safe file deletion and manipulation functions with protection checks
|
|
|
|
#Requires -Version 5.1
|
|
Set-StrictMode -Version Latest
|
|
|
|
# Prevent multiple sourcing
|
|
if ((Get-Variable -Name 'MOLE_FILEOPS_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_FILEOPS_LOADED) { return }
|
|
$script:MOLE_FILEOPS_LOADED = $true
|
|
|
|
# Import dependencies
|
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
. "$scriptDir\base.ps1"
|
|
. "$scriptDir\log.ps1"
|
|
|
|
# ============================================================================
|
|
# Global State
|
|
# ============================================================================
|
|
|
|
$script:MoleDryRunMode = $env:MOLE_DRY_RUN -eq "1"
|
|
$script:TotalSizeCleaned = 0
|
|
$script:FilesCleaned = 0
|
|
$script:TotalItems = 0
|
|
|
|
# ============================================================================
|
|
# Safety Validation Functions
|
|
# ============================================================================
|
|
|
|
function Test-SafePath {
|
|
<#
|
|
.SYNOPSIS
|
|
Validate that a path is safe to operate on
|
|
.DESCRIPTION
|
|
Checks against protected paths and whitelist
|
|
.OUTPUTS
|
|
$true if safe, $false if protected
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Path
|
|
)
|
|
|
|
# Must have a path
|
|
if ([string]::IsNullOrWhiteSpace($Path)) {
|
|
Write-Debug "Empty path rejected"
|
|
return $false
|
|
}
|
|
|
|
# Resolve to full path
|
|
$fullPath = Resolve-SafePath -Path $Path
|
|
if (-not $fullPath) {
|
|
Write-Debug "Could not resolve path: $Path"
|
|
return $false
|
|
}
|
|
|
|
# Check protected paths
|
|
if (Test-ProtectedPath -Path $fullPath) {
|
|
Write-Debug "Protected path rejected: $fullPath"
|
|
return $false
|
|
}
|
|
|
|
# Check whitelist
|
|
if (Test-Whitelisted -Path $fullPath) {
|
|
Write-Debug "Whitelisted path rejected: $fullPath"
|
|
return $false
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Get-PathSize {
|
|
<#
|
|
.SYNOPSIS
|
|
Get the size of a file or directory in bytes
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not (Test-Path $Path)) {
|
|
return 0
|
|
}
|
|
|
|
try {
|
|
if (Test-Path $Path -PathType Container) {
|
|
$size = (Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue |
|
|
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($null -eq $size) { return 0 }
|
|
return [long]$size
|
|
}
|
|
else {
|
|
return (Get-Item $Path -Force -ErrorAction SilentlyContinue).Length
|
|
}
|
|
}
|
|
catch {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
function Get-PathSizeKB {
|
|
<#
|
|
.SYNOPSIS
|
|
Get the size of a file or directory in kilobytes
|
|
#>
|
|
param([string]$Path)
|
|
|
|
$bytes = Get-PathSize -Path $Path
|
|
return [Math]::Ceiling($bytes / 1024)
|
|
}
|
|
|
|
# ============================================================================
|
|
# Safe Removal Functions
|
|
# ============================================================================
|
|
|
|
function Remove-SafeItem {
|
|
<#
|
|
.SYNOPSIS
|
|
Safely remove a file or directory with all protection checks
|
|
.DESCRIPTION
|
|
This is the main safe deletion function. It:
|
|
- Validates the path is not protected
|
|
- Checks against whitelist
|
|
- Supports dry-run mode
|
|
- Tracks cleanup statistics
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Path,
|
|
|
|
[string]$Description = "",
|
|
|
|
[switch]$Force,
|
|
|
|
[switch]$Recurse
|
|
)
|
|
|
|
# Validate path safety
|
|
if (-not (Test-SafePath -Path $Path)) {
|
|
Write-Debug "Skipping protected/whitelisted path: $Path"
|
|
return $false
|
|
}
|
|
|
|
# Check if path exists
|
|
if (-not (Test-Path $Path)) {
|
|
Write-Debug "Path does not exist: $Path"
|
|
return $false
|
|
}
|
|
|
|
# Get size before removal
|
|
$size = Get-PathSize -Path $Path
|
|
$sizeKB = [Math]::Ceiling($size / 1024)
|
|
$sizeHuman = Format-ByteSize -Bytes $size
|
|
|
|
# Handle dry run
|
|
if ($script:MoleDryRunMode) {
|
|
$name = if ($Description) { $Description } else { Split-Path -Leaf $Path }
|
|
Write-DryRun "$name $($script:Colors.Yellow)($sizeHuman dry)$($script:Colors.NC)"
|
|
Set-SectionActivity
|
|
return $true
|
|
}
|
|
|
|
# Perform removal
|
|
try {
|
|
$isDirectory = Test-Path $Path -PathType Container
|
|
|
|
if ($isDirectory) {
|
|
Remove-Item -Path $Path -Recurse -Force -ErrorAction Stop
|
|
}
|
|
else {
|
|
Remove-Item -Path $Path -Force -ErrorAction Stop
|
|
}
|
|
|
|
# Update statistics
|
|
$script:TotalSizeCleaned += $sizeKB
|
|
$script:FilesCleaned++
|
|
$script:TotalItems++
|
|
|
|
# Log success
|
|
$name = if ($Description) { $Description } else { Split-Path -Leaf $Path }
|
|
Write-Success "$name $($script:Colors.Green)($sizeHuman)$($script:Colors.NC)"
|
|
Set-SectionActivity
|
|
|
|
return $true
|
|
}
|
|
catch {
|
|
Write-Debug "Failed to remove $Path : $_"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Remove-SafeItems {
|
|
<#
|
|
.SYNOPSIS
|
|
Safely remove multiple items with a collective description
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string[]]$Paths,
|
|
|
|
[string]$Description = "Items"
|
|
)
|
|
|
|
$totalSize = 0
|
|
$removedCount = 0
|
|
$failedCount = 0
|
|
|
|
foreach ($path in $Paths) {
|
|
if (-not (Test-SafePath -Path $path)) {
|
|
continue
|
|
}
|
|
|
|
if (-not (Test-Path $path)) {
|
|
continue
|
|
}
|
|
|
|
$size = Get-PathSize -Path $path
|
|
|
|
if ($script:MoleDryRunMode) {
|
|
$totalSize += $size
|
|
$removedCount++
|
|
continue
|
|
}
|
|
|
|
try {
|
|
$isDirectory = Test-Path $path -PathType Container
|
|
if ($isDirectory) {
|
|
Remove-Item -Path $path -Recurse -Force -ErrorAction Stop
|
|
}
|
|
else {
|
|
Remove-Item -Path $path -Force -ErrorAction Stop
|
|
}
|
|
$totalSize += $size
|
|
$removedCount++
|
|
}
|
|
catch {
|
|
$failedCount++
|
|
Write-Debug "Failed to remove: $path - $_"
|
|
}
|
|
}
|
|
|
|
if ($removedCount -gt 0) {
|
|
$sizeKB = [Math]::Ceiling($totalSize / 1024)
|
|
$sizeHuman = Format-ByteSize -Bytes $totalSize
|
|
|
|
if ($script:MoleDryRunMode) {
|
|
Write-DryRun "$Description $($script:Colors.Yellow)($removedCount items, $sizeHuman dry)$($script:Colors.NC)"
|
|
}
|
|
else {
|
|
$script:TotalSizeCleaned += $sizeKB
|
|
$script:FilesCleaned += $removedCount
|
|
$script:TotalItems++
|
|
Write-Success "$Description $($script:Colors.Green)($removedCount items, $sizeHuman)$($script:Colors.NC)"
|
|
}
|
|
Set-SectionActivity
|
|
}
|
|
|
|
return @{
|
|
Removed = $removedCount
|
|
Failed = $failedCount
|
|
Size = $totalSize
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Pattern-Based Cleanup Functions
|
|
# ============================================================================
|
|
|
|
function Remove-OldFiles {
|
|
<#
|
|
.SYNOPSIS
|
|
Remove files older than specified days
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Path,
|
|
|
|
[int]$DaysOld = 7,
|
|
|
|
[string]$Filter = "*",
|
|
|
|
[string]$Description = "Old files"
|
|
)
|
|
|
|
if (-not (Test-Path $Path)) {
|
|
return @{ Removed = 0; Size = 0 }
|
|
}
|
|
|
|
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
|
|
|
|
$oldFiles = Get-ChildItem -Path $Path -Filter $Filter -File -Force -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.LastWriteTime -lt $cutoffDate }
|
|
|
|
if ($oldFiles) {
|
|
$paths = $oldFiles | ForEach-Object { $_.FullName }
|
|
return Remove-SafeItems -Paths $paths -Description "$Description (>${DaysOld}d old)"
|
|
}
|
|
|
|
return @{ Removed = 0; Size = 0 }
|
|
}
|
|
|
|
function Remove-EmptyDirectories {
|
|
<#
|
|
.SYNOPSIS
|
|
Remove empty directories recursively
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Path,
|
|
|
|
[string]$Description = "Empty directories"
|
|
)
|
|
|
|
if (-not (Test-Path $Path)) {
|
|
return @{ Removed = 0 }
|
|
}
|
|
|
|
$removedCount = 0
|
|
$maxIterations = 5
|
|
|
|
for ($i = 0; $i -lt $maxIterations; $i++) {
|
|
$emptyDirs = Get-ChildItem -Path $Path -Directory -Recurse -Force -ErrorAction SilentlyContinue |
|
|
Where-Object {
|
|
(Get-ChildItem -Path $_.FullName -Force -ErrorAction SilentlyContinue | Measure-Object).Count -eq 0
|
|
}
|
|
|
|
if (-not $emptyDirs -or $emptyDirs.Count -eq 0) {
|
|
break
|
|
}
|
|
|
|
foreach ($dir in $emptyDirs) {
|
|
if (Test-SafePath -Path $dir.FullName) {
|
|
if (-not $script:MoleDryRunMode) {
|
|
try {
|
|
Remove-Item -Path $dir.FullName -Force -ErrorAction Stop
|
|
$removedCount++
|
|
}
|
|
catch {
|
|
Write-Debug "Could not remove empty dir: $($dir.FullName)"
|
|
}
|
|
}
|
|
else {
|
|
$removedCount++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($removedCount -gt 0) {
|
|
if ($script:MoleDryRunMode) {
|
|
Write-DryRun "$Description $($script:Colors.Yellow)($removedCount dirs dry)$($script:Colors.NC)"
|
|
}
|
|
else {
|
|
Write-Success "$Description $($script:Colors.Green)($removedCount dirs)$($script:Colors.NC)"
|
|
}
|
|
Set-SectionActivity
|
|
}
|
|
|
|
return @{ Removed = $removedCount }
|
|
}
|
|
|
|
function Clear-DirectoryContents {
|
|
<#
|
|
.SYNOPSIS
|
|
Clear all contents of a directory but keep the directory itself
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Path,
|
|
|
|
[string]$Description = ""
|
|
)
|
|
|
|
if (-not (Test-Path $Path)) {
|
|
return @{ Removed = 0; Size = 0 }
|
|
}
|
|
|
|
if (-not (Test-SafePath -Path $Path)) {
|
|
return @{ Removed = 0; Size = 0 }
|
|
}
|
|
|
|
$items = Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue
|
|
if ($items) {
|
|
$paths = $items | ForEach-Object { $_.FullName }
|
|
$desc = if ($Description) { $Description } else { Split-Path -Leaf $Path }
|
|
return Remove-SafeItems -Paths $paths -Description $desc
|
|
}
|
|
|
|
return @{ Removed = 0; Size = 0 }
|
|
}
|
|
|
|
# ============================================================================
|
|
# Statistics Functions
|
|
# ============================================================================
|
|
|
|
function Get-CleanupStats {
|
|
<#
|
|
.SYNOPSIS
|
|
Get current cleanup statistics
|
|
#>
|
|
return @{
|
|
TotalSizeKB = $script:TotalSizeCleaned
|
|
TotalSizeHuman = Format-ByteSize -Bytes ($script:TotalSizeCleaned * 1024)
|
|
FilesCleaned = $script:FilesCleaned
|
|
TotalItems = $script:TotalItems
|
|
}
|
|
}
|
|
|
|
function Reset-CleanupStats {
|
|
<#
|
|
.SYNOPSIS
|
|
Reset cleanup statistics
|
|
#>
|
|
$script:TotalSizeCleaned = 0
|
|
$script:FilesCleaned = 0
|
|
$script:TotalItems = 0
|
|
}
|
|
|
|
function Set-DryRunMode {
|
|
<#
|
|
.SYNOPSIS
|
|
Enable or disable dry-run mode
|
|
#>
|
|
param([bool]$Enabled)
|
|
$script:MoleDryRunMode = $Enabled
|
|
}
|
|
|
|
function Test-DryRunMode {
|
|
<#
|
|
.SYNOPSIS
|
|
Check if dry-run mode is enabled
|
|
#>
|
|
return $script:MoleDryRunMode
|
|
}
|
|
|
|
# ============================================================================
|
|
# Exports (functions are available via dot-sourcing)
|
|
# ============================================================================
|
|
# Functions: Test-SafePath, Get-PathSize, Remove-SafeItem, etc.
|