Private/Backup-Item.ps1

# Internal helper -- copy a single file or directory into the backup tree.
# Returns a PSCustomObject result row for the caller to accumulate into its
# results table. Never throws -- copy failures degrade to Status='FAIL' rows.

function Backup-Item {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Source,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$BackupDir,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$DestSubPath,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Label,
        [switch]$IsDir
    )

    $dest = Join-Path $BackupDir $DestSubPath
    try {
        if ($IsDir) {
            if (Test-Path -LiteralPath $Source) {
                # [G4] sec F3: if the source directory IS a reparse point at the
                # top level, refuse to follow it -- the operator passed a path
                # that resolves to somewhere else, which is almost certainly
                # not what they wanted backed up.
                $srcItem = Get-Item -LiteralPath $Source -Force -ErrorAction Stop
                if ($srcItem.LinkType) {
                    Write-Host " [SKIP] $Label ($($srcItem.LinkType) target $($srcItem.Target) -- not following)" -ForegroundColor Yellow
                    return [PSCustomObject]@{ Item = $Label; Status = 'SKIP'; Detail = "$($srcItem.LinkType) -- not followed" }
                }
                $destParent = Split-Path $dest -Parent
                # [G4] sec F8: explicit -ErrorAction Stop so caller's
                # ErrorActionPreference inheritance can't bypass our try/catch.
                New-Item -ItemType Directory -Force -Path $destParent -ErrorAction Stop | Out-Null
                Copy-Item -Recurse -Force -LiteralPath $Source -Destination $dest -ErrorAction Stop
                $count = (Get-ChildItem -Recurse -LiteralPath $dest -File -Attributes !ReparsePoint -ErrorAction SilentlyContinue).Count
                Write-Host " [OK] $Label ($count files)" -ForegroundColor Green
                return [PSCustomObject]@{ Item = $Label; Status = 'OK'; Detail = "$count files" }
            } else {
                Write-Host " [SKIP] $Label (not found: $Source)" -ForegroundColor Yellow
                return [PSCustomObject]@{ Item = $Label; Status = 'SKIP'; Detail = 'Not found' }
            }
        } else {
            if (Test-Path -LiteralPath $Source) {
                $destParent = Split-Path $dest -Parent
                New-Item -ItemType Directory -Force -Path $destParent -ErrorAction Stop | Out-Null
                Copy-Item -Force -LiteralPath $Source -Destination $dest -ErrorAction Stop
                $size = (Get-Item -LiteralPath $dest).Length
                Write-Host " [OK] $Label ($size bytes)" -ForegroundColor Green
                return [PSCustomObject]@{ Item = $Label; Status = 'OK'; Detail = "$size bytes" }
            } else {
                Write-Host " [SKIP] $Label (not found: $Source)" -ForegroundColor Yellow
                return [PSCustomObject]@{ Item = $Label; Status = 'SKIP'; Detail = 'Not found' }
            }
        }
    } catch {
        Write-Host " [FAIL] $Label ($_)" -ForegroundColor Red
        return [PSCustomObject]@{ Item = $Label; Status = 'FAIL'; Detail = $_.Exception.Message }
    }
}