Pipelines III: Caso práctico y composición

Abstract

...

...

Calcular hash de archivos en streaming con begin , process y end

Este ejemplo muestra un caso pipeline-aware realista (aunque algo simplificado para no abrumar): calcular el hash de archivos sin cargar todo en memoria. El objetivo es ilustrar la separación de responsabilidades entre los bloques begin , process y end , más que profundizar en criptografía.

Política de algoritmos y flags de seguridad

Antes de hacer streaming del archivo, conviene definir una política de selección de algoritmo. El siguiente bloque no calcula el hash; valida y describe la elección del algoritmo según si es considerado inseguro (MD5, SHA1), si fue autorizado explícitamente con un flag y si entra en la categoría de “fuerte” (SHA256, SHA384, SHA512). Esto permite fallar temprano o registrar decisiones, y luego encadenar el cómputo real en otra función.

Comprobación de política de hash
scripts/pipeline/hash/Test-AlgorithmSecurity.ps1
#Requires -Version 7.0
[CmdletBinding()]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias('Name', 'HashAlgorithm')]
    [ValidateSet('MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512')]
    [string] $Algorithm = 'SHA256',

    [switch] $AllowInsecure
)

process {
    $isInsecure = $Algorithm -in @('MD5', 'SHA1')
    $isSecure = !$isInsecure
    $isAllowed = $isSecure -or $isInsecure -and $AllowInsecure

    [PSCustomObject]@{
        Algorithm  = $Algorithm
        IsInsecure = $isInsecure
        IsAllowed  = $isAllowed
        IsSecure   = $isSecure
    }
}
Emite un objeto con campos Algorithm, IsInsecure, IsAllowed, IsSecure, IsStrongHash. Útil para registrar/validar antes del cómputo en streaming.

¿Qué acabamos de hacer?

  • Entrada por pipeline (combinada): el parámetro $Algorithm acepta entrada por ValueFromPipeline y por ValueFromPipelineByPropertyName simultáneamente. Con los [Alias('Name','HashAlgorithm')] es común que encaje con objetos previos (p. ej. salidas que traen un campo Name o HashAlgorithm), sin tener que renombrar propiedades.
  • [ValidateSet(...)] : restringe la entrada a una lista conocida de algoritmos y habilita autocompletado, mensajes de error claros y documentación implícita.
Función Get-FileHashStream (pipeline-aware)
scripts/pipeline/Get-FileHashStream.ps1
#Requires -Version 7.0
[CmdletBinding()]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias('FullName', 'Path')]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })]
    [string] $LiteralPath,

    [ValidateSet('MD5', 'SHA1', 'SHA256')]
    [string] $Algorithm = 'SHA256',

    [switch] $IncludeLength
)

begin {
    switch ($Algorithm) {
        'SHA256' { $hasher = [System.Security.Cryptography.SHA256]::Create(); break }
        'SHA1' { $hasher = [System.Security.Cryptography.SHA1]::Create(); break }
        'MD5' { $hasher = [System.Security.Cryptography.MD5]::Create(); break }
        default { throw "Unsupported algorithm: $Algorithm" }
    }

    Write-Verbose "Initialized $Algorithm hasher."
}

process {
    try {
        $resolved = (Resolve-Path -LiteralPath $LiteralPath -ErrorAction Stop).Path
        $file = Get-Item -LiteralPath $resolved -ErrorAction Stop

        $stream = [System.IO.File]::OpenRead($resolved)
        try {
            $hasher.Initialize() | Out-Null
            $bytes = $hasher.ComputeHash($stream)
            $hashString = [Convert]::ToHexString($bytes).ToLowerInvariant()

            $result = @{
                Path      = $file.FullName
                Algorithm = $Algorithm
                Hash      = $hashString
            }
            if ($IncludeLength) { $result.Length = $file.Length }

            [pscustomobject]$result
        }
        finally {
            $stream.Dispose()
        }
    }
    catch {
        Write-Warning ("Could not hash '{0}': {1}" -f $LiteralPath, $_.Exception.Message)
    }
}

end {
    if ($hasher) {
        $hasher.Dispose()
        Write-Verbose "Disposed $Algorithm hasher."
    }
}
Entrada por pipeline (acepta FullName/Path por alias) y salida de objetos: Path, Algorithm, [Length], Hash.

¿Qué acabamos de hacer?

  • Entrada por pipeline combinada: los atributos ValueFromPipeline y ValueFromPipelineByPropertyName pueden usarse juntos en el mismo parámetro. Esto permite que el script acepte tanto la entrada directa (el objeto completo que fluye por el pipeline) como la asignación automática de una propiedad que coincida por nombre. Por ejemplo, un parámetro con [Alias('FullName','Path')] podrá recibir objetos FileInfo desde Get-ChildItem , ya sea porque el objeto completo se inyecta o porque PowerShell detecta y usa su propiedad FullName . Esta combinación maximiza la compatibilidad con distintos productores y evita tener que usar ForEach-Object o Select-Object solo para adaptar nombres.
  • [ValidateSet(...)] : restringe $Algorithm a valores conocidos (SHA256|SHA1|MD5). Mejora mensajes de error y autocompletado.
  • switch (...) { ... } es una estructura de control para comparar un valor contra múltiples condiciones. Evalúa una expresión y ejecuta el bloque asociado a la primera coincidencia (o a todas, si no se usa break ).
    Sintaxis básica
    switch ($value) {
        'A' { 'Matched A'; break }
        'B' { 'Matched B'; break }
        default { 'No match' }
    }
    • default se ejecuta si ninguna condición coincide.
    • break detiene la ejecución dentro del switch , evitando que siga evaluando otros casos.

    Precaución

    No debe confundirse con el tipo de parámetro [switch] , que representa un flag booleano (p. ej., -Verbose o -Force ) y no tiene relación con la estructura condicional switch .
  • begin = inicialización única: se crea el hasher una sola vez y se reutiliza. Si el algoritmo no es válido, se falla temprano.
  • process = unidad de trabajo: por cada archivo se resuelve la ruta, se abre un FileStream , se inicializa el hasher y se calcula el hash. Se emite un [pscustomobject] por elemento, listo para componer.
  • end = limpieza: se liberan recursos compartidos como el hasher. Los flujos individuales se cierran en su propio finally para cada elemento.
  • Detalles clave:
    • [System.IO.File]::OpenRead($resolved) abre el archivo en modo lectura.
    • $hasher.Initialize() reinicia el estado antes de cada elemento.
    • $hasher.ComputeHash($stream) calcula el hash de forma eficiente.
    • [Convert]::ToHexString($bytes).ToLowerInvariant() lo convierte a hex.
    • $stream.Dispose() en finally garantiza cierre del archivo incluso con errores.
    • $hasher.Dispose() en end libera recursos al finalizar el pipeline.
Desde scripts/pipeline
# Archivos de la carpeta actual -> calcular hash SHA256 e incluir longitud
Get-ChildItem -File |
    .\Get-FileHashStream.ps1 -Algorithm SHA256 -IncludeLength |
    Sort-Object Length -Descending |
    Select-Object -First 2 @{
        Name='FileName'
        Expression={ Split-Path $_.Path -Leaf }
    }, Length, Hash |
    Format-Table -AutoSize
Lista los 2 archivos más grandes en la carpeta actual, mostrando su nombre, longitud y hash SHA256. Select-Object usa una expresión para extraer solo el nombre del archivo ( Split-Path $_.Path -Leaf ).

Ejercicio: Ejercicio

Parte B. Compón un pipeline que use: Get-ChildItem → tu script de la Parte A → .\Get-FileHashStream.ps1 -Algorithm SHA256 para calcular el hash de cada archivo → proyecta columnas y exporta a CSV. La idea es agregar la propiedad Hash sin romper el flujo.

Para exportar a CSV, usa | Export-Csv -LiteralPath "$Env:TMP\file-inventory.csv" , que crea un archivo temporal en la carpeta del usuario. Luego, abre ese archivo con code $Env:TMP\file-inventory.csv para ver el resultado.

Hints

  • Propiedades calculadas con Select-Object : puedes crear columnas derivadas en tiempo real usando la sintaxis: @{ Name = 'NombreColumna'; Expression = { <bloque de código> } } .
  • Desglose de la expresión:
    • $_ representa el elemento actual del pipeline.
    • Get-FileHashStream.ps1 -Algorithm SHA256 -LiteralPath $_.Path invoca el script sobre la ruta del archivo actual.
    • Select-Object -ExpandProperty Hash extrae solo el valor del hash (sin cabeceras ni estructura adicional).
  • Una vez que tengas el resultado de Get-FileHashStream.ps1 , usa | Select-Object -ExpandProperty Hash para proyectar solo el valor del hash (en lugar de @{Hash = xx} ).

Solución

Solución Parte B — composición de pipeline
# Archivos de la carpeta actual -> resumen + SHA256 -> CSV
Get-ChildItem -File |
    .\Get-FileInfoSummary.ps1 |
    Select-Object Name, Path, Length, LastWriteTime, @{
        Name       = 'Hash'
        Expression = {
            # Invoca el hasher por elemento y extrae solo el valor del hash
            (.\Get-FileHashStream.ps1 -Algorithm SHA256 -LiteralPath $_.Path |
                Select-Object -ExpandProperty Hash)
        }
    } |
    Export-Csv "$Env:TMP\file-inventory.csv"
El Select-Object usa una propiedad calculada para anexar Hash por elemento.