Files
zf-sql/RestoreScript.ps1
T
2026-06-24 12:28:01 +01:00

731 lines
29 KiB
PowerShell

param(
[Parameter(Mandatory=$true)]
[string]$LiveMountRoot,
[Parameter(Mandatory=$false)]
[string]$DatabaseName,
[Parameter(Mandatory=$true)]
[ValidateSet("catalog", "restore", "verify")]
[string]$Action,
[Parameter(Mandatory=$false)]
[string]$SqlInstance = "sqlfcsql\TESTINST",
[Parameter(Mandatory=$false)]
[string]$DataPath,
[Parameter(Mandatory=$false)]
[string]$LogPath,
[Parameter(Mandatory=$false)]
[string]$NewName,
[Parameter(Mandatory=$false)]
[string]$StopAtTime,
[Parameter(Mandatory=$false)]
[string]$StopAtLSN,
[Parameter(Mandatory=$false)]
[switch]$PreviewRestorePlan
)
# Function to catalog backups
function Catalog-Backups {
param([string]$Path)
$backupFiles = Get-ChildItem -Path $Path -Recurse -File | Where-Object { $_.Extension -eq '.bak' -or $_.Extension -eq '.trn' }
$catalog = @{}
foreach ($file in $backupFiles) {
$baseName = $file.BaseName
$parts = $baseName -split '_'
if ($parts.Length -lt 4) { continue }
# Find the type position (FULL, DIFF, LOG)
$typeIndex = -1
$validTypes = @('FULL', 'DIFF', 'LOG')
for ($i = 0; $i -lt $parts.Length; $i++) {
if ($validTypes -contains $parts[$i]) {
$typeIndex = $i
break
}
}
if ($typeIndex -eq -1 -or ($parts.Length - $typeIndex) -lt 3) { continue }
# Assume parts[0] is prefix if typeIndex > 1, else dbName is parts[0]
if ($typeIndex -eq 1) {
$dbName = $parts[0]
} else {
$dbName = $parts[1..($typeIndex-1)] -join '_'
}
$type = $parts[$typeIndex]
$dateStr = $parts[$typeIndex + 1]
$timeStr = $parts[$typeIndex + 2]
$stripe = 0
if ($parts.Length -gt $typeIndex + 3) {
$stripeVal = 0
if (-not [int]::TryParse($parts[$typeIndex + 3], [ref]$stripeVal)) {
continue
}
$stripe = $stripeVal
}
$key = "$dateStr$timeStr"
if (-not $catalog.ContainsKey($dbName)) { $catalog[$dbName] = @{} }
if (-not $catalog[$dbName].ContainsKey($type)) { $catalog[$dbName][$type] = @{} }
if (-not $catalog[$dbName][$type].ContainsKey($key)) { $catalog[$dbName][$type][$key] = @() }
$catalog[$dbName][$type][$key] += @{File = $file; Stripe = $stripe}
}
return $catalog
}
# Function to report catalog
function Report-Catalog {
param([hashtable]$Catalog)
Write-Host "Database Backups Catalog:"
Write-Host "========================="
foreach ($db in $catalog.Keys | Sort-Object) {
Write-Host "Database: $db"
foreach ($type in $catalog[$db].Keys | Sort-Object) {
Write-Host " Type: $type"
foreach ($key in $catalog[$db][$type].Keys | Sort-Object -Descending) {
Write-Host " Backup: $key"
$files = $catalog[$db][$type][$key] | Sort-Object { $_.Stripe }
foreach ($item in $files) {
Write-Host " $($item.File.FullName)"
}
}
}
Write-Host ""
}
Write-Host "Summary of Databases:"
$catalog.Keys | Sort-Object | ForEach-Object { Write-Host " - $_" }
}
# Function to restore database
function Restore-Database {
param(
[string]$DbName,
[hashtable]$Catalog,
[string]$Instance,
[string]$DataPath,
[string]$LogPath,
[string]$TargetDbName,
[string]$StopAtTime,
[string]$StopAtLSN,
[bool]$PreviewRestorePlan
)
$restoreDbName = if ($TargetDbName) { $TargetDbName } else { $DbName }
$shouldRenamePhysicalFiles = -not [string]::IsNullOrWhiteSpace($TargetDbName) -and ($TargetDbName -ne $DbName)
$isPointInTimeRestore = ($StopAtTime -or $StopAtLSN)
$targetStopAtTime = $null
$targetStopAtLsn = $null
if (-not $catalog.ContainsKey($DbName)) {
Write-Error "Database $DbName not found in catalog"
return
}
$dbCatalog = $catalog[$DbName]
# Find the latest FULL backup
if (-not $dbCatalog.ContainsKey('FULL')) {
Write-Error "No FULL backup found for database $DbName"
return
}
if ($StopAtTime) {
try {
# Parse using the current OS locale so users can enter dates in their own format.
$targetStopAtTime = [datetime]::Parse($StopAtTime, [System.Globalization.CultureInfo]::CurrentCulture)
} catch {
Write-Error "StopAtTime '$StopAtTime' is not a valid datetime value (expected format for your locale: $([System.Globalization.CultureInfo]::CurrentCulture.DateTimeFormat.ShortDatePattern) HH:mm:ss)"
return
}
}
if ($StopAtLSN) {
$lsnForCompare = if ($StopAtLSN -like 'lsn:*') { $StopAtLSN.Substring(4) } else { $StopAtLSN }
try {
$targetStopAtLsn = [decimal]$lsnForCompare
} catch {
Write-Error "StopAtLSN '$StopAtLSN' must be a numeric LSN value"
return
}
}
# Select FULL backup. For StopAtTime, choose latest FULL that finished at/before target time.
$fullCandidates = @()
$fullKeys = $dbCatalog['FULL'].Keys | Sort-Object
foreach ($key in $fullKeys) {
$files = $dbCatalog['FULL'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$header = Get-BackupInfo -Files $files -Instance $Instance
if (-not $header) {
Write-Warning "Skipping FULL backup $key for $DbName because its header could not be read"
continue
}
$finish = $null
try {
$finish = [datetime]$header.BackupFinishDate
} catch {
Write-Warning "Skipping FULL backup $key for $DbName because BackupFinishDate could not be parsed"
continue
}
if ($targetStopAtTime -and $finish -gt $targetStopAtTime) {
continue
}
$fullCandidates += @{
Key = $key
Files = $files
Header = $header
Finish = $finish
}
}
if ($fullCandidates.Count -eq 0) {
if ($targetStopAtTime) {
Write-Error "No FULL backup found that is at or before StopAtTime '$StopAtTime' for database $DbName"
} else {
Write-Error "No usable FULL backup found for database $DbName"
}
return
}
$selectedFull = if ($targetStopAtTime) {
$fullCandidates | Sort-Object { $_.Finish } -Descending | Select-Object -First 1
} else {
$fullCandidates | Sort-Object { $_.Key } -Descending | Select-Object -First 1
}
$latestFullKey = $selectedFull.Key
$fullFiles = $selectedFull.Files
$fullHeader = $selectedFull.Header
$fullBaseLsn = if ($fullHeader.CheckpointLSN) { [decimal]$fullHeader.CheckpointLSN } else { [decimal]$fullHeader.FirstLSN }
# Create directories if specified
if ($DataPath -and -not (Test-Path $DataPath)) {
New-Item -ItemType Directory -Path $DataPath -Force
}
if ($LogPath -and -not (Test-Path $LogPath)) {
New-Item -ItemType Directory -Path $LogPath -Force
}
# Get file list for MOVE clauses
$fileListStr = $fullFiles | ForEach-Object { "DISK = '$($_.FullName)'" }
$fileListOnlyQuery = "RESTORE FILELISTONLY FROM $($fileListStr -join ', ')"
$fileListResult = Invoke-Sqlcmd -ServerInstance $Instance -Query $fileListOnlyQuery -QueryTimeout 0 -ErrorAction Stop
$moveClauses = @()
$dataFileIndex = 0
$logFileIndex = 0
foreach ($file in $fileListResult) {
$logicalName = $file.LogicalName
$type = $file.Type
$originalName = $file.PhysicalName
$extension = [System.IO.Path]::GetExtension($originalName)
$baseName = [System.IO.Path]::GetFileName($originalName)
# When restoring as a different database name, rewrite physical file names
# so they do not clash with the source database files on disk.
if ($shouldRenamePhysicalFiles) {
if ($type -eq 'D') {
$dataFileIndex++
$newBaseName = if ($dataFileIndex -eq 1) { "${restoreDbName}_Data" } else { "${restoreDbName}_Data$dataFileIndex" }
$baseName = "$newBaseName$extension"
} elseif ($type -eq 'L') {
$logFileIndex++
$newBaseName = if ($logFileIndex -eq 1) { "${restoreDbName}_Log" } else { "${restoreDbName}_Log$logFileIndex" }
$baseName = "$newBaseName$extension"
}
}
if ($type -eq 'D' -and $DataPath) {
$newPath = Join-Path $DataPath $baseName
$moveClauses += "MOVE '$logicalName' TO '$newPath'"
} elseif ($type -eq 'L' -and $LogPath) {
$newPath = Join-Path $LogPath $baseName
$moveClauses += "MOVE '$logicalName' TO '$newPath'"
}
}
# Build FULL restore query
$withClause = if ($moveClauses) { "WITH $($moveClauses -join ', '), NORECOVERY" } else { "WITH NORECOVERY" }
$restoreQuery = "RESTORE DATABASE [$restoreDbName] FROM $($fileListStr -join ', ') $withClause"
$fullRestoreQuery = $restoreQuery
# Choose the latest DIFF that is compatible with the selected FULL.
$selectedDiff = $null
$selectedDiffKey = $null
if ($dbCatalog.ContainsKey('DIFF')) {
$compatibleDiffs = @()
$diffKeys = $dbCatalog['DIFF'].Keys | Where-Object { $_ -gt $latestFullKey } | Sort-Object
foreach ($key in $diffKeys) {
$diffFiles = $dbCatalog['DIFF'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$diffHeader = Get-BackupInfo -Files $diffFiles -Instance $Instance
if (-not $diffHeader) {
Write-Warning "Skipping DIFF backup $key for $DbName because its header could not be read"
continue
}
if ([decimal]$diffHeader.DifferentialBaseLSN -ne $fullBaseLsn) {
continue
}
$diffFinish = $null
try {
$diffFinish = [datetime]$diffHeader.BackupFinishDate
} catch {
Write-Warning "Skipping DIFF backup $key for $DbName because BackupFinishDate could not be parsed"
continue
}
if ($targetStopAtTime -and $diffFinish -gt $targetStopAtTime) {
continue
}
$compatibleDiffs += @{ Key = $key; Files = $diffFiles; Finish = $diffFinish }
}
if ($compatibleDiffs.Count -gt 0) {
$selectedDiff = if ($targetStopAtTime) {
$compatibleDiffs | Sort-Object { $_.Finish } -Descending | Select-Object -First 1
} else {
$compatibleDiffs | Sort-Object { $_.Key } -Descending | Select-Object -First 1
}
$selectedDiffKey = $selectedDiff.Key
}
}
$logStartKey = if ($selectedDiffKey) { $selectedDiffKey } else { $latestFullKey }
$logKeys = @()
# Determine the LastLSN of the last backup that will be restored (FULL or DIFF).
$lastRestoredHeader = if ($selectedDiff) {
Get-BackupInfo -Files $selectedDiff.Files -Instance $Instance
} else {
$fullHeader
}
$lastRestoredLsn = if ($lastRestoredHeader) { [decimal]$lastRestoredHeader.LastLSN } else { $null }
# Select LOG backups after the latest restored FULL/DIFF point.
if ($dbCatalog.ContainsKey('LOG')) {
$candidateLogKeys = @($dbCatalog['LOG'].Keys | Where-Object { $_ -gt $logStartKey } | Sort-Object)
# Validate LSN continuity: the first candidate LOG must connect to the end of the FULL/DIFF chain.
# If there is a gap (first LOG's FirstLSN > FULL/DIFF LastLSN), no logs can be applied.
if ($candidateLogKeys.Count -gt 0 -and $lastRestoredLsn) {
$firstLogFiles = $dbCatalog['LOG'][$candidateLogKeys[0]] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$firstLogHeader = Get-BackupInfo -Files $firstLogFiles -Instance $Instance
if ($firstLogHeader -and [decimal]$firstLogHeader.FirstLSN -gt $lastRestoredLsn) {
Write-Warning "LSN gap detected: first available LOG backup ($($candidateLogKeys[0])) starts at LSN $($firstLogHeader.FirstLSN), but the restored FULL/DIFF ends at LSN $lastRestoredLsn. No log backups can be applied."
$candidateLogKeys = @()
}
}
$skipLogSelection = $false
if ($isPointInTimeRestore -and $candidateLogKeys.Count -eq 0) {
# Only error if the target point is beyond the last restored backup's finish time.
# If the target is at or before that time, no logs are required and restore can proceed.
$lastRestoredFinish = if ($lastRestoredHeader) {
try { [datetime]$lastRestoredHeader.BackupFinishDate } catch { $null }
} else { $null }
$targetIsBeyondRestored = $true
if ($targetStopAtTime -and $lastRestoredFinish -and $targetStopAtTime -le $lastRestoredFinish) {
$targetIsBeyondRestored = $false
}
if ($targetStopAtLsn -and $lastRestoredLsn -and $targetStopAtLsn -le $lastRestoredLsn) {
$targetIsBeyondRestored = $false
}
if ($targetIsBeyondRestored) {
Write-Error "Point-in-time restore requested but no LOG backups are available after the selected FULL/DIFF restore point"
return
}
# Target is within the FULL/DIFF range; no logs needed.
$skipLogSelection = $true
}
if (-not $skipLogSelection -and $StopAtTime) {
$cutoffFound = $false
foreach ($key in $candidateLogKeys) {
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$logHeader = Get-BackupInfo -Files $logFiles -Instance $Instance
if (-not $logHeader) {
continue
}
$logStartTime = $null
$logFinishTime = $null
try {
$logStartTime = [datetime]$logHeader.BackupStartDate
$logFinishTime = [datetime]$logHeader.BackupFinishDate
} catch {
continue
}
if ($logFinishTime -lt $targetStopAtTime) {
$logKeys += $key
continue
}
# A log backup contains all records up to BackupFinishDate. The first log
# whose finish time meets/exceeds StopAtTime is the PITR cutoff log.
if ($logFinishTime -ge $targetStopAtTime) {
$logKeys += $key
$cutoffFound = $true
break
}
}
if (-not $cutoffFound) {
Write-Error "StopAtTime '$StopAtTime' is not covered by available LOG backups after the selected FULL/DIFF restore point"
return
}
} elseif (-not $skipLogSelection -and $StopAtLSN) {
$cutoffFound = $false
foreach ($key in $candidateLogKeys) {
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$logHeader = Get-BackupInfo -Files $logFiles -Instance $Instance
if (-not $logHeader) {
continue
}
$logFirstLsn = $null
$logLastLsn = $null
try {
$logFirstLsn = [decimal]$logHeader.FirstLSN
$logLastLsn = [decimal]$logHeader.LastLSN
} catch {
continue
}
if ($logLastLsn -lt $targetStopAtLsn) {
$logKeys += $key
continue
}
if ($logFirstLsn -le $targetStopAtLsn -and $targetStopAtLsn -le $logLastLsn) {
$logKeys += $key
$cutoffFound = $true
break
}
if ($logFirstLsn -gt $targetStopAtLsn) {
break
}
}
if (-not $cutoffFound) {
Write-Error "StopAtLSN '$StopAtLSN' is not covered by available LOG backups after the selected FULL/DIFF restore point"
return
}
} else {
$logKeys = $candidateLogKeys
}
if (-not $skipLogSelection -and $isPointInTimeRestore -and $logKeys.Count -eq 0) {
Write-Error "Point-in-time restore requested but no usable LOG backups were selected"
return
}
} elseif ($isPointInTimeRestore) {
Write-Error "Point-in-time restore requested but this database has no LOG backups in the catalog"
return
}
if ($PreviewRestorePlan) {
Write-Host ""
Write-Host "Restore Plan Preview"
Write-Host "===================="
Write-Host "Source Database: $DbName"
Write-Host "Target Database: $restoreDbName"
Write-Host "FULL Backup Key: $latestFullKey"
Write-Host "FULL Backup Files:"
foreach ($f in $fullFiles) {
Write-Host " - $($f.FullName)"
}
if ($selectedDiffKey) {
Write-Host "Selected DIFF Key: $selectedDiffKey"
Write-Host "Selected DIFF Files:"
foreach ($f in $selectedDiff.Files) {
Write-Host " - $($f.FullName)"
}
} else {
Write-Host "Selected DIFF Key: none"
}
if ($logKeys.Count -gt 0) {
Write-Host "LOG Range: $($logKeys[0]) -> $($logKeys[-1]) ($($logKeys.Count) backup sets)"
} else {
Write-Host "LOG Range: none"
}
if ($isPointInTimeRestore) {
if ($StopAtTime) {
Write-Host "PITR Target: STOPAT '$($targetStopAtTime.ToString([System.Globalization.CultureInfo]::CurrentCulture))'"
} else {
$previewLsn = if ($StopAtLSN -like 'lsn:*') { $StopAtLSN } else { "lsn:$StopAtLSN" }
Write-Host "PITR Target: STOPATMARK '$previewLsn'"
}
} else {
Write-Host "PITR Target: none (restore to latest available point)"
}
Write-Host "MOVE Clauses:"
foreach ($m in $moveClauses) {
Write-Host " - $m"
}
Write-Host ""
Write-Host "PreviewRestorePlan enabled. Exiting without executing restore operations."
return
}
if ($shouldRenamePhysicalFiles) {
Write-Host "Restoring FULL backup for $DbName as $restoreDbName..."
} else {
Write-Host "Restoring FULL backup for $DbName..."
}
Invoke-Sqlcmd -ServerInstance $Instance -Query $fullRestoreQuery -QueryTimeout 0 -ErrorAction Stop
if ($selectedDiffKey) {
$fileList = $selectedDiff.Files | ForEach-Object { "DISK = '$($_.FullName)'" }
$restoreQuery = "RESTORE DATABASE [$restoreDbName] FROM $($fileList -join ', ') WITH NORECOVERY"
Write-Host "Applying DIFF backup $selectedDiffKey for $restoreDbName..."
Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop
}
if ($logKeys.Count -gt 0) {
for ($i = 0; $i -lt $logKeys.Count; $i++) {
$key = $logKeys[$i]
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$fileList = $logFiles | ForEach-Object { "DISK = '$($_.FullName)'" }
$logWithClause = "WITH NORECOVERY"
# Apply STOPAT/STOPATMARK only on the final LOG restore statement.
if ($isPointInTimeRestore -and $i -eq ($logKeys.Count - 1)) {
if ($StopAtTime) {
# Use the parsed datetime formatted as ISO 8601, not the raw input string.
# SQL Server requires a consistent datetime format; the user's input may be
# locale-specific (e.g. dd/MM/yyyy) which SQL Server does not accept.
$sqlStopAt = $targetStopAtTime.ToString("yyyy-MM-dd HH:mm:ss")
$logWithClause = "$logWithClause, STOPAT = '$sqlStopAt'"
} elseif ($StopAtLSN) {
$normalizedLsn = if ($StopAtLSN -like "lsn:*") { $StopAtLSN } else { "lsn:$StopAtLSN" }
$escapedLsn = $normalizedLsn.Replace("'", "''")
$logWithClause = "$logWithClause, STOPATMARK = '$escapedLsn'"
}
}
$restoreQuery = "RESTORE LOG [$restoreDbName] FROM $($fileList -join ', ') $logWithClause"
Write-Host "Applying LOG backup $key for $restoreDbName..."
Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop
}
}
# Final recovery
$restoreQuery = "RESTORE DATABASE [$restoreDbName] WITH RECOVERY"
Write-Host "Finalizing restore for $restoreDbName..."
Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop
Write-Host "Restore completed for $restoreDbName"
}
# Function to print backup summary
function Print-BackupSummary {
param([string]$DbName, [hashtable]$Headers)
Write-Host "Backup Summary for database: $DbName"
Write-Host "==================================="
# FULL backups
if ($Headers.ContainsKey('FULL')) {
Write-Host "FULL Backups:"
$fullItems = @($Headers['FULL'] | Sort-Object { $_.Key })
foreach ($item in $fullItems) {
$header = @($item.Header)[0]
if (-not $header) { continue }
Write-Host " Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
}
}
# DIFF backups
if ($Headers.ContainsKey('DIFF')) {
Write-Host "DIFFERENTIAL Backups:"
$diffItems = @($Headers['DIFF'] | Sort-Object { $_.Key })
foreach ($item in $diffItems) {
$header = @($item.Header)[0]
if (-not $header) { continue }
Write-Host " Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) | Base LSN: $($header.DifferentialBaseLSN) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
}
}
# LOG backups
if ($Headers.ContainsKey('LOG')) {
$logItems = @($Headers['LOG'] | Sort-Object { $_.Key })
if ($logItems.Count -gt 0) {
$firstLog = @($logItems[0].Header)[0]
$lastLog = @($logItems[-1].Header)[0]
Write-Host "LOG Backups:"
Write-Host " Point-in-Time Range: $(([datetime]$firstLog.BackupStartDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) to $(([datetime]$lastLog.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
Write-Host " LSN Range: $($firstLog.FirstLSN) - $($lastLog.LastLSN)"
# Check for gaps
$gaps = @()
for ($i = 1; $i -lt $logItems.Count; $i++) {
$prevHeader = @($logItems[$i-1].Header)[0]
$currHeader = @($logItems[$i].Header)[0]
if (-not $prevHeader -or -not $currHeader) { continue }
$prevLast = $prevHeader.LastLSN
$currFirst = $currHeader.FirstLSN
if ($prevLast -ne $currFirst) {
$gaps += "Gap between $($logItems[$i-1].Key) (LSN $($prevLast)) and $($logItems[$i].Key) (LSN $($currFirst))"
}
}
if ($gaps.Count -gt 0) {
Write-Host " *** MISSING RANGES ***"
foreach ($gap in $gaps) {
Write-Host " $gap"
}
} else {
Write-Host " No gaps detected in LOG sequence"
}
}
}
if (-not ($Headers.ContainsKey('FULL') -or $Headers.ContainsKey('DIFF') -or $Headers.ContainsKey('LOG'))) {
Write-Host "No backup headers retrieved"
}
}
# Function to verify backups
function Verify-Backups {
param([hashtable]$Catalog, [string]$Instance)
foreach ($db in $catalog.Keys | Sort-Object) {
Write-Host "Verifying backups for database: $db"
$headers = @{}
foreach ($type in $catalog[$db].Keys | Sort-Object) {
foreach ($key in $catalog[$db][$type].Keys | Sort-Object) {
$files = $catalog[$db][$type][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$fileList = $files | ForEach-Object { "DISK = '$($_.FullName)'" }
$verifyQuery = "RESTORE VERIFYONLY FROM $($fileList -join ', ')"
Write-Host "Verifying $type backup $key for $db..."
$verified = $false
try {
Invoke-Sqlcmd -ServerInstance $Instance -Query $verifyQuery -QueryTimeout 0 -ErrorAction Stop
Write-Host "Verification successful for $type $key"
$verified = $true
} catch {
Write-Host "Verification failed for $type $key : $($_.Exception.Message)"
}
# Get backup header information only if verified
if ($verified) {
$header = Get-BackupInfo -Files $files -Instance $Instance
if ($header) {
Write-Host " Backup Details:"
Write-Host " Start Date: $(([datetime]$header.BackupStartDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
Write-Host " Finish Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
Write-Host " First LSN: $($header.FirstLSN)"
Write-Host " Last LSN: $($header.LastLSN)"
if ($header.DatabaseBackupLSN) {
Write-Host " Database Backup LSN: $($header.DatabaseBackupLSN)"
}
if ($header.DifferentialBaseLSN) {
Write-Host " Differential Base LSN: $($header.DifferentialBaseLSN)"
}
# Collect headers for summary
if (-not $headers.ContainsKey($type)) { $headers[$type] = @() }
$headers[$type] += @{ Key = $key; Header = $header }
}
}
}
}
# Print summary
Print-BackupSummary -DbName $db -Headers $headers
Write-Host ""
}
}
# Function to get backup header information
function Get-BackupInfo {
param([System.IO.FileInfo[]]$Files, [string]$Instance)
$fileList = $Files | ForEach-Object { "DISK = '$($_.FullName)'" }
$headerQuery = "RESTORE HEADERONLY FROM $($fileList -join ', ')"
try {
$header = Invoke-Sqlcmd -ServerInstance $Instance -Query $headerQuery -QueryTimeout 0 -ErrorAction Stop
return $header
} catch {
Write-Warning "Failed to get header for $($Files[0].Name): $($_.Exception.Message)"
return $null
}
}
# Main script
if ($Action -ne "restore" -and $NewName) {
Write-Error "NewName is only valid with restore action"
exit 1
}
if ($StopAtTime -and $StopAtLSN) {
Write-Error "Specify only one of StopAtTime or StopAtLSN"
exit 1
}
if ($Action -ne "restore" -and ($StopAtTime -or $StopAtLSN)) {
Write-Error "StopAtTime and StopAtLSN are only valid with restore action"
exit 1
}
if ($Action -ne "restore" -and $PreviewRestorePlan) {
Write-Error "PreviewRestorePlan is only valid with restore action"
exit 1
}
if ($Action -eq "catalog") {
$catalog = Catalog-Backups -Path $LiveMountRoot
if ($DatabaseName) {
$filteredCatalog = @{}
if ($catalog.ContainsKey($DatabaseName)) {
$filteredCatalog[$DatabaseName] = $catalog[$DatabaseName]
}
Report-Catalog -Catalog $filteredCatalog
} else {
Report-Catalog -Catalog $catalog
}
} elseif ($Action -eq "restore") {
if (-not $DatabaseName) {
Write-Error "DatabaseName is required for restore action"
exit 1
}
if (-not $DataPath -or -not $LogPath) {
Write-Error "DataPath and LogPath are required for restore action"
exit 1
}
$catalog = Catalog-Backups -Path $LiveMountRoot
Restore-Database -DbName $DatabaseName -Catalog $catalog -Instance $SqlInstance -DataPath $DataPath -LogPath $LogPath -TargetDbName $NewName -StopAtTime $StopAtTime -StopAtLSN $StopAtLSN -PreviewRestorePlan $PreviewRestorePlan
} elseif ($Action -eq "verify") {
$catalog = Catalog-Backups -Path $LiveMountRoot
if ($DatabaseName) {
$filteredCatalog = @{}
if ($catalog.ContainsKey($DatabaseName)) {
$filteredCatalog[$DatabaseName] = $catalog[$DatabaseName]
}
} else {
$filteredCatalog = $catalog
}
Verify-Backups -Catalog $filteredCatalog -Instance $SqlInstance
}