1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 19:44:44 +00:00
Files
Mole/lib/core/file_ops.ps1

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.