Private/Restore-BackupFile.ps1

function Restore-BackupFile
{
    <#
    .SYNOPSIS
        Restores a single backup ZIP file to a specified location.

    .DESCRIPTION
        Extracts a backup ZIP file to a destination directory, optionally using
        metadata information to restore original paths, timestamps, and attributes.
        This is an internal helper function used by Restore-DailyBackup.

    .PARAMETER BackupFilePath
        The full path to the backup ZIP file to restore.

    .PARAMETER DestinationPath
        The destination directory where the backup should be restored.

    .PARAMETER UseOriginalPath
        If specified and metadata is available, attempts to restore to the original
        source path rather than the specified destination.

    .PARAMETER PreservePaths
        Controls whether directory structure within the ZIP is preserved during extraction.

    .PARAMETER VerboseEnabled
        Controls verbose output during the restore operation.

    .OUTPUTS
        [PSCustomObject]
        Returns information about the restore operation including success status,
        paths processed, and any errors encountered.

    .NOTES
        This function leverages PowerShell's Expand-Archive cmdlet for extraction
        and attempts to restore file attributes and timestamps when possible.

    .EXAMPLE
        PS > Restore-BackupFile -BackupFilePath 'D:\Backups\2025-09-15\Documents.zip' -DestinationPath 'C:\Restored'

        Extracts the Documents backup to C:\Restored
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [string] $BackupFilePath,

        [Parameter()]
        [string] $DestinationPath,

        [Parameter()]
        [switch] $UseOriginalPath,

        [Parameter()]
        [switch] $PreservePaths,

        [Parameter()]
        [bool] $VerboseEnabled = $false
    )

    if (-not $UseOriginalPath -and -not $DestinationPath)
    {
        throw 'Either DestinationPath must be specified or UseOriginalPath must be enabled'
    }

    if (-not (Test-Path $BackupFilePath))
    {
        throw "Backup file not found: $BackupFilePath"
    }

    $backupName = [System.IO.Path]::GetFileNameWithoutExtension($BackupFilePath)
    $metadataPath = Join-Path (Split-Path $BackupFilePath) "$backupName.metadata.json"

    $metadata = $null
    if (Test-Path $metadataPath)
    {
        try
        {
            $metadata = Get-Content $metadataPath -Raw | ConvertFrom-Json
            Write-Verbose "Restore-BackupFile> Loaded metadata for $backupName"
        }
        catch
        {
            Write-Warning "Failed to read metadata for $backupName : $_"
        }
    }

    # Determine restore destination
    $finalDestination = if ($UseOriginalPath -and $metadata -and $metadata.SourcePath)
    {
        if ($metadata.PathType -eq 'File')
        {
            Split-Path $metadata.SourcePath
        }
        else
        {
            $metadata.SourcePath
        }
    }
    elseif ($DestinationPath)
    {
        $DestinationPath
    }
    else
    {
        throw 'Cannot determine destination path: UseOriginalPath is enabled but no metadata source path available, and no DestinationPath specified'
    }

    Write-Verbose "Restore-BackupFile> Final destination determined as: $finalDestination"

    # Ensure destination exists
    if (-not (Test-Path $finalDestination))
    {
        if ($PSCmdlet.ShouldProcess($finalDestination, 'Create Directory'))
        {
            New-Item -Path $finalDestination -ItemType Directory -Force | Out-Null
            Write-Verbose "Restore-BackupFile> Created destination directory: $finalDestination"
        }
    }

    # Extract the backup
    if ($PSCmdlet.ShouldProcess($BackupFilePath, 'Expand-Archive'))
    {
        try
        {
            Write-Verbose "Restore-BackupFile> Extracting $BackupFilePath to $finalDestination"

            if ($PreservePaths)
            {
                Expand-Archive -Path $BackupFilePath -DestinationPath $finalDestination -Force
            }
            else
            {
                # Extract to a temp location first, then move files to preserve structure
                $tempDir = if ($env:TEMP) { $env:TEMP } elseif ($env:TMP) { $env:TMP } elseif ($env:TMPDIR) { $env:TMPDIR } else { '/tmp' }
                $tempPath = Join-Path $tempDir "DailyBackupRestore_$(Get-Random)"
                Write-Verbose "Restore-BackupFile> Extracting to temp path: $tempPath"
                Expand-Archive -Path $BackupFilePath -DestinationPath $tempPath -Force

                # Verify temp path exists and has content
                if (-not (Test-Path $tempPath))
                {
                    throw "Extraction failed: temp path $tempPath does not exist"
                }

                Write-Verbose "Restore-BackupFile> Temp path contents: $(Get-ChildItem $tempPath -Name)"

                # Move extracted items to final destination
                $extractedItems = Get-ChildItem $tempPath -Recurse
                foreach ($item in $extractedItems)
                {
                    if ($item.PSIsContainer)
                    {
                        $targetDir = Join-Path $finalDestination $item.Name
                        if (-not (Test-Path $targetDir))
                        {
                            New-Item -Path $targetDir -ItemType Directory -Force | Out-Null
                        }
                    }
                    else
                    {
                        $targetFile = Join-Path $finalDestination $item.Name
                        Copy-Item $item.FullName $targetFile -Force
                    }
                }

                # Clean up temp directory
                Remove-Item $tempPath -Recurse -Force -ErrorAction SilentlyContinue
            }

            # Attempt to restore metadata if available
            if ($metadata -and $metadata.PathType -eq 'File' -and $metadata.LastWriteTime)
            {
                try
                {
                    $restoredFiles = Get-ChildItem $finalDestination -File -Recurse
                    foreach ($file in $restoredFiles)
                    {
                        $file.LastWriteTime = [DateTime]::Parse($metadata.LastWriteTime)
                    }
                    Write-Verbose 'Restore-BackupFile> Restored file timestamps'
                }
                catch
                {
                    Write-Warning "Failed to restore file timestamps: $_"
                }
            }

            return [PSCustomObject]@{
                Success = $true
                SourcePath = $BackupFilePath
                DestinationPath = $finalDestination
                Metadata = $metadata
                Message = "Successfully restored $backupName"
            }
        }
        catch
        {
            return [PSCustomObject]@{
                Success = $false
                SourcePath = $BackupFilePath
                DestinationPath = $finalDestination
                Metadata = $metadata
                Message = "Failed to restore $backupName : $_"
            }
        }
    }
    else
    {
        return [PSCustomObject]@{
            Success = $true
            SourcePath = $BackupFilePath
            DestinationPath = $finalDestination
            Metadata = $metadata
            Message = "Dry-run: Would restore $backupName to $finalDestination"
        }
    }
}