Limpieza de disco y automatización de mantenimiento

Descripción general

En este artículo se muestran las tareas periódicas de limpieza de disco en Windows Server divididas en cuatro categorías y se explica cómo automatizarlas mediante scripts independientes de PowerShell para cada propósito:

  • Optimización de WinSxS (reducción del almacén de componentes)
  • Limpieza de carpetas temporales (por días de retención)
  • Rotación de archivos de registro (varias rutas, cada una con su propio período de retención)
  • Copia de seguridad y limpieza de registros de eventos

Convención de variables

Nombre de la variable Ejemplo Comentario
<<ADMIN_USER>> Administrator Usuario con el que se ejecutan las tareas programadas
<<LOG_PATH>> C:\Maintenance\Logs Directorio común de registros de ejecución
<<BACKUP_PATH>> C:\Maintenance\Backups Directorio de copia de seguridad de registros de eventos
<<DAILY_TEMP_TASK_NAME>> DailyTempCleanup Nombre de la tarea de limpieza de Temp
<<DAILY_LOG_TASK_NAME>> DailyLogRotation Nombre de la tarea de rotación de logs
<<MONTHLY_WINSXS_TASK_NAME>> MonthlyWinSxSCleanup Nombre de la tarea de optimización de WinSxS
<<MONTHLY_EVENTLOG_TASK_NAME>> MonthlyEventLogMaintenance Nombre de la tarea de mantenimiento de registros de eventos

Step 1: Resumen de la optimización de la carpeta WinSxS

C:\Windows\WinSxS es el almacén de componentes (component store) donde se conserva el historial de actualizaciones y componentes. Tiende a crecer con el tiempo y puede optimizarse con los siguientes comandos DISM:

# Análisis del almacén de componentes
Dism /Online /Cleanup-Image /AnalyzeComponentStore

# Limpieza de componentes antiguos
Dism /Online /Cleanup-Image /StartComponentCleanup

# Eliminación completa de versiones antiguas (sin posibilidad de rollback)
Dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase

Como /ResetBase impide desinstalar actualizaciones antiguas, lo más seguro es ejecutarlo únicamente en momentos planificados, por ejemplo una vez al mes durante una ventana de mantenimiento.


Step 2: Resumen de la limpieza de carpetas temporales

En $env:TEMP y C:\Windows\Temp se acumulan archivos temporales creados por instaladores y aplicaciones. En lugar de eliminarlos todos sin distinción, en la práctica es más razonable aplicar la política:

“eliminar archivos más antiguos que X días”.


Step 3: Resumen de la rotación de archivos de registro

Los directorios de logs crecen con el número de aplicaciones y con el tiempo pueden consumir mucho espacio de disco. Como es deseable establecer días de retención específicos por directorio, utilizaremos un archivo de configuración PSD1 para definir múltiples rutas y su retención de forma flexible.


Step 4: Resumen del mantenimiento de registros de eventos

Los registros de eventos son fundamentales para el análisis de incidencias y la auditoría, por lo que no deben limpiarse con frecuencia sin copia de seguridad previa. Un patrón habitual, ejecutado aproximadamente una vez al mes, es exportar primero a .evtx y luego limpiar el registro.

# Forma básica de copia de seguridad y limpieza
wevtutil epl System C:\Logs\System_20250101.evtx
wevtutil cl System

Step 5: Directrices de diseño de los scripts

En este artículo se definen cuatro scripts, uno para cada objetivo:

  1. cleanup_temp.ps1
    Elimina archivos antiguos en carpetas temporales (días de retención configurables como parámetro).

  2. rotate_logs.ps1
    Elimina archivos .log en los directorios definidos en un archivo de configuración PSD1.

  3. optimize_winsxs.ps1
    Ejecuta /StartComponentCleanup /ResetBase en WinSxS y registra la cantidad de espacio liberado.

  4. maintain_eventlogs.ps1
    Exporta determinados registros de eventos a .evtx y posteriormente limpia dichos registros.

Con esta estructura, cada script se puede ejecutar de forma independiente desde el Programador de tareas, lo que permite combinar tareas diarias/mensuales y ajustar la política con flexibilidad.

Estructura de directorios de ejemplo:

C:\
└─ Maintenance\
   ├─ cleanup_temp.ps1               # Limpieza de carpetas Temp (diaria)
   ├─ rotate_logs.ps1                # Rotación de logs (diaria)
   ├─ optimize_winsxs.ps1            # Optimización de WinSxS (mensual)
   ├─ maintain_eventlogs.ps1         # Copia de seguridad + limpieza de registros de eventos (mensual)
   │
   ├─ log_rotation.psd1              # Configuración de rotación (varias rutas + días)
   │
   ├─ Logs\                          # Registros de ejecución de cada script
   │    ├─ cleanup_temp_*.log
   │    ├─ rotate_logs_*.log
   │    ├─ optimize_winsxs_*.log
   │    └─ eventlog_maint_*.log
   │
   └─ Backups\                       # Copias de seguridad de registros de eventos (EVTX)
        ├─ System_YYYYMMDD.evtx
        ├─ Application_YYYYMMDD.evtx
        └─ Security_YYYYMMDD.evtx

Step 6: Script de limpieza de Temp

Este script elimina archivos y carpetas más antiguos que DaysToKeep días bajo los directorios definidos en $TempPaths, y registra los resultados en un archivo de log.
Los parámetros DaysToKeep y LogPath pueden sobrescribirse en la llamada.

cleanup_temp.ps1

param(
    [string[]]$TempPaths = @("$env:TEMP", "C:\Windows\Temp"),
    [int]$DaysToKeep = 7,
    [string]$LogPath = "<<LOG_PATH>>"
)

if (-not (Test-Path $LogPath)) {
    New-Item $LogPath -ItemType Directory -Force | Out-Null
}

$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$logFile = Join-Path $LogPath "cleanup_temp_$timestamp.log"

function Write-Log($Message) {
    "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message |
        Tee-Object -FilePath $logFile -Append
}

Write-Log "===== Temp cleanup started ====="
Write-Log "DaysToKeep = $DaysToKeep"

$limitDate = (Get-Date).AddDays(-$DaysToKeep)

foreach ($path in $TempPaths) {
    if (-not (Test-Path $path)) {
        Write-Log "Skip: Not found -> $path"
        continue
    }

    Write-Log "Processing: $path"

    Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue |
        Where-Object { $_.LastWriteTime -lt $limitDate } |
        ForEach-Object {
            try {
                Remove-Item -LiteralPath $_.FullName -Force -Recurse -ErrorAction Stop
                Write-Log "Deleted: $($_.FullName)"
            }
            catch {
                Write-Log "Failed: $($_.FullName) - $($_.Exception.Message)"
            }
        }
}

Write-Log "===== Temp cleanup finished ====="

Step 7: Script de rotación de logs

Archivo de configuración de rotación (PSD1)

Este archivo PSD1 define varios directorios de destino y sus correspondientes días de retención. Para añadir un nuevo directorio de logs, basta con incluir una nueva entrada.

log_rotation.psd1

@{
    RotationTargets = @(
        @{
            Path       = "C:\Logs\App1"
            DaysToKeep = 7
        },
        @{
            Path       = "C:\Logs\App2"
            DaysToKeep = 30
        },
        @{
            Path       = "<<LOG_PATH>>"
            DaysToKeep = 7
        }
    )
}

Script de rotación de logs

Este script lee RotationTargets desde el PSD1 y elimina los archivos .log en cada ruta cuyo LastWriteTime sea anterior al límite calculado. Todas las acciones se registran en un archivo de log.

rotate_logs.ps1

param(
    [string]$ConfigPath = "C:\Maintenance\log_rotation.psd1",
    [string]$LogPath = "<<LOG_PATH>>"
)

if (-not (Test-Path $LogPath)) {
    New-Item $LogPath -ItemType Directory -Force | Out-Null
}

$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$logFile = Join-Path $LogPath "rotate_logs_$timestamp.log"

function Write-Log($Message) {
    "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message |
        Tee-Object -FilePath $logFile -Append
}

Write-Log "===== Log rotation started ====="
Write-Log "ConfigPath = $ConfigPath"

if (-not (Test-Path $ConfigPath)) {
    Write-Log "Config file not found. Exit."
    exit 1
}

try {
    $config = Import-PowerShellDataFile -Path $ConfigPath
}
catch {
    Write-Log "Config load failed: $($_.Exception.Message)"
    exit 1
}

foreach ($target in $config.RotationTargets) {
    $targetPath = $target.Path
    $daysToKeep = [int]$target.DaysToKeep

    if (-not (Test-Path $targetPath)) {
        Write-Log "Skip (not found): $targetPath"
        continue
    }

    $limitDate = (Get-Date).AddDays(-$daysToKeep)
    Write-Log "Path=$targetPath DaysToKeep=$daysToKeep Limit=$limitDate"

    Get-ChildItem -Path $targetPath -Recurse -Include *.log -ErrorAction SilentlyContinue |
        Where-Object { $_.LastWriteTime -lt $limitDate } |
        ForEach-Object {
            try {
                Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop
                Write-Log "Deleted: $($_.FullName)"
            }
            catch {
                Write-Log "Failed: $($_.FullName) - $($_.Exception.Message)"
            }
        }
}

Write-Log "===== Log rotation finished ====="

Step 8: Script de optimización de WinSxS

Este script mide el tamaño del directorio WinSxS antes y después de la limpieza y ejecuta /StartComponentCleanup /ResetBase. De esta forma, se puede comprobar numéricamente cuánto espacio se ha recuperado.

optimize_winsxs.ps1

param(
    [string]$LogPath = "<<LOG_PATH>>"
)

if (-not (Test-Path $LogPath)) {
    New-Item $LogPath -ItemType Directory -Force | Out-Null
}

$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$logFile = Join-Path $LogPath "optimize_winsxs_$timestamp.log"

function Write-Log($Message) {
    "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message |
        Tee-Object -FilePath $logFile -Append
}

Write-Log "===== WinSxS optimization started ====="

$winsxsPath = "C:\Windows\WinSxS"

Write-Log "Measuring WinSxS size before..."
$sizeBefore = (Get-ChildItem $winsxsPath -Recurse -Force -ErrorAction SilentlyContinue |
               Measure-Object Length -Sum).Sum
$sizeBeforeGB = [math]::Round($sizeBefore / 1GB, 2)
Write-Log "Before: $sizeBeforeGB GB"

Dism /Online /Cleanup-Image /AnalyzeComponentStore |
    Out-File (Join-Path $LogPath "dism_before_$timestamp.txt")

Write-Log "Running StartComponentCleanup /ResetBase..."
Dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase |
    Out-File (Join-Path $LogPath "dism_exec_$timestamp.txt")

Write-Log "Measuring WinSxS size after..."
$sizeAfter = (Get-ChildItem $winsxsPath -Recurse -Force -ErrorAction SilentlyContinue |
              Measure-Object Length -Sum).Sum
$sizeAfterGB = [math]::Round($sizeAfter / 1GB, 2)

Write-Log "After: $sizeAfterGB GB"
Write-Log ("Reduced: {0} GB" -f ([math]::Round(($sizeBefore - $sizeAfter)/1GB,2)))

Dism /Online /Cleanup-Image /AnalyzeComponentStore |
    Out-File (Join-Path $LogPath "dism_after_$timestamp.txt")

Write-Log "===== WinSxS optimization finished ====="

Step 9: Script de mantenimiento de registros de eventos

Este script exporta los registros especificados (por defecto: System / Application / Security) a archivos .evtx en un directorio de copia de seguridad y luego limpia los registros originales. El archivo de log detalla todos los éxitos y errores.

maintain_eventlogs.ps1

param(
    [string]$LogPath = "<<LOG_PATH>>",                  # Directorio de registros (.log)
    [string]$BackupPath = "<<BACKUP_PATH>>",            # Directorio de backup EVTX
    [string[]]$EventLogs = @("System", "Application", "Security")
)

# Crear el directorio de logs, si es necesario
if (-not (Test-Path $LogPath)) {
    New-Item $LogPath -ItemType Directory -Force | Out-Null
}

# Crear el directorio de backup, si es necesario
if (-not (Test-Path $BackupPath)) {
    New-Item $BackupPath -ItemType Directory -Force | Out-Null
}

$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$logFile = Join-Path $LogPath "eventlog_maint_$timestamp.log"

function Write-Log($Message) {
    "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message |
        Tee-Object -FilePath $logFile -Append
}

Write-Log "===== Event log maintenance started ====="
Write-Log "BackupPath = $BackupPath"

foreach ($name in $EventLogs) {

    # Generar un nombre de archivo EVTX único por log
    $destEvtx = Join-Path $BackupPath ("{0}_{1}.evtx" -f $name, $timestamp)

    try {
        Write-Log "Export: $name -> $destEvtx"
        wevtutil epl $name $destEvtx

        Write-Log "Clear: $name"
        wevtutil cl $name
    }
    catch {
        Write-Log "Failed: $name - $($_.Exception.Message)"
    }
}

Write-Log "===== Event log maintenance finished ====="

Step 10: Ejemplos de registro en el Programador de tareas

A continuación, todos los scripts de mantenimiento se registran usando schtasks.exe.
Para una explicación detallada de las opciones /SC, /D, /ST, /RU, etc., consulte:

Administración del Programador de tareas con schtasks.exe


Ejemplo de tarea diaria de limpieza de Temp

Ejecuta cleanup_temp.ps1 todos los días a las 2:00, eliminando archivos temporales más antiguos que 7 días.
La tarea se ejecuta como <<ADMIN_USER>> con máximos privilegios (/RL HIGHEST).

schtasks /Create `
  /TN "<<DAILY_TEMP_TASK_NAME>>" `
  /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Maintenance\cleanup_temp.ps1 -DaysToKeep 7 -LogPath <<LOG_PATH>>" `
  /SC DAILY `
  /ST 02:00 `
  /RU "<<ADMIN_USER>>" `
  /RL HIGHEST `
  /F

Ejemplo de tarea diaria de rotación de logs

Ejecuta rotate_logs.ps1 todos los días a las 2:30 y rota los archivos .log de varios directorios según la configuración definida en el PSD1.
Los días de retención se controlan exclusivamente a través de log_rotation.psd1.

schtasks /Create `
  /TN "<<DAILY_LOG_TASK_NAME>>" `
  /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Maintenance\rotate_logs.ps1 -ConfigPath C:\Maintenance\log_rotation.psd1 -LogPath <<LOG_PATH>>" `
  /SC DAILY `
  /ST 02:30 `
  /RU "<<ADMIN_USER>>" `
  /RL HIGHEST `
  /F

Ejemplo de tarea mensual de optimización de WinSxS

Ejecuta optimize_winsxs.ps1 el día 1 de cada mes a las 3:00, realizando StartComponentCleanup /ResetBase con medición del tamaño.
Dado que no existe posibilidad de rollback, este script debe ejecutarse dentro de una ventana de mantenimiento planificada.

schtasks /Create `
  /TN "<<MONTHLY_WINSXS_TASK_NAME>>" `
  /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Maintenance\optimize_winsxs.ps1 -LogPath <<LOG_PATH>>" `
  /SC MONTHLY `
  /D 1 `
  /ST 03:00 `
  /RU "<<ADMIN_USER>>" `
  /RL HIGHEST `
  /F

Ejemplo de tarea mensual de mantenimiento de registros de eventos

Ejecuta maintain_eventlogs.ps1 el día 1 de cada mes a las 3:30, exportando registros como System / Application / Security al directorio de copia de seguridad y limpiándolos a continuación.
Ajuste -EventLogs y -BackupPath según sus requisitos de auditoría.

schtasks /Create `
  /TN "<<MONTHLY_EVENTLOG_TASK_NAME>>" `
  /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Maintenance\maintain_eventlogs.ps1 -LogPath <<LOG_PATH>>" `
  /SC MONTHLY `
  /D 1 `
  /ST 03:30 `
  /RU "<<ADMIN_USER>>" `
  /RL HIGHEST `
  /F

Resumen

En este artículo se ha mostrado cómo dividir el mantenimiento de disco en Windows Server en scripts específicos por objetivo y cómo utilizar un archivo de configuración PSD1 para controlar de forma flexible los destinos de rotación de logs y sus períodos de retención.

  • Limpieza de Temp: diaria, con días de retención configurables
  • Rotación de logs: varios caminos y retenciones definidos en PSD1
  • Optimización de WinSxS: mensual, con registro del espacio liberado
  • Mantenimiento de registros de eventos: mensual, con copia de seguridad en EVTX seguida de limpieza

Con ello se obtiene una automatización de mantenimiento robusta y extensible para entornos de producción.