Skip to content

Instantly share code, notes, and snippets.

@agurod42
Created January 11, 2026 18:44
Show Gist options
  • Select an option

  • Save agurod42/dbf8209b9d3779177051e5f92834fb97 to your computer and use it in GitHub Desktop.

Select an option

Save agurod42/dbf8209b9d3779177051e5f92834fb97 to your computer and use it in GitHub Desktop.
SQL Server Backup & Restore Tool — Download .bak files from remote SQL Server and restore to local instance. No dependencies, no config files, all via parameters.
<#
.SYNOPSIS
SQL Server Database Backup & Restore Tool
.DESCRIPTION
Download database backups from a remote SQL Server and restore them to a local instance.
All configuration is passed via parameters - no config files required.
.PARAMETER Command
Action to perform: backup, restore, or info
.PARAMETER SourceServer
Source SQL Server address (for backup download)
.PARAMETER SourceUser
Source SQL Server username
.PARAMETER SourcePassword
Source SQL Server password
.PARAMETER TargetServer
Target SQL Server address for restore (default: localhost)
.PARAMETER TargetUser
Target SQL Server username (default: SA)
.PARAMETER TargetPassword
Target SQL Server password
.PARAMETER Database
Database name to backup/restore (can be comma-separated for multiple)
.PARAMETER Databases
Array of database names to backup/restore
.PARAMETER TargetDatabase
Target database name (defaults to same as Database)
.PARAMETER BackupDir
Local directory for backup files (default: ./backups)
.PARAMETER BackupFile
Specific backup file to restore (optional, auto-detects if not provided)
.PARAMETER DataDir
SQL Server data directory for restored files (default: auto-detect)
.PARAMETER MaxSizeMB
Maximum backup size to download in MB (default: 500)
.PARAMETER Force
Overwrite existing backup files / Skip confirmations
.EXAMPLE
# Download backup from remote server
.\sqlserver-db-tools.ps1 backup `
-SourceServer "myserver.database.windows.net" `
-SourceUser "admin" `
-SourcePassword "secret" `
-Database "MyDatabase"
.EXAMPLE
# Download multiple databases at once
.\sqlserver-db-tools.ps1 backup `
-SourceServer "myserver.database.windows.net" `
-SourceUser "admin" `
-SourcePassword "secret" `
-Databases "DB1,DB2,DB3"
.EXAMPLE
# Run directly from GitHub Gist (one-liner)
irm https://gist.github.com/USER/ID/raw | iex; Invoke-SqlBackup -Server "srv" -User "u" -Pass "p" -Databases "DB1,DB2"
.EXAMPLE
# Restore to local SQL Server
.\sqlserver-db-tools.ps1 restore `
-TargetServer "localhost" `
-TargetUser "SA" `
-TargetPassword "secret" `
-Database "MyDatabase"
.EXAMPLE
# Full pipeline: download and restore with different target name
.\sqlserver-db-tools.ps1 backup -SourceServer "remote" -SourceUser "u" -SourcePassword "p" -Database "ProdDB"
.\sqlserver-db-tools.ps1 restore -TargetServer "localhost" -TargetUser "SA" -TargetPassword "p" -Database "ProdDB" -TargetDatabase "ProdDB_Local"
#>
[CmdletBinding()]
param(
[Parameter(Position=0, Mandatory=$true)]
[ValidateSet("backup", "restore", "info")]
[string]$Command,
# Source server (for backup)
[string]$SourceServer,
[string]$SourceUser,
[string]$SourcePassword,
# Target server (for restore)
[string]$TargetServer = "localhost",
[string]$TargetUser = "SA",
[string]$TargetPassword,
# Database(s) - supports comma-separated list
[string]$Database,
[string[]]$Databases,
[string]$TargetDatabase,
# Paths
[string]$BackupDir = ".\backups",
[string]$BackupFile,
[string]$DataDir,
# Options
[int]$MaxSizeMB = 500,
[switch]$Force
)
# =============================================================================
# OUTPUT HELPERS
# =============================================================================
function Write-Status {
param(
[string]$Type,
[string]$Message
)
$config = @{
"OK" = @{ Color = "Green"; Prefix = "[OK]" }
"WARN" = @{ Color = "Yellow"; Prefix = "[!!]" }
"ERR" = @{ Color = "Red"; Prefix = "[XX]" }
"INFO" = @{ Color = "Cyan"; Prefix = "[->]" }
"HEAD" = @{ Color = "Blue"; Prefix = "===" }
}
$c = $config[$Type]
Write-Host $c.Prefix -ForegroundColor $c.Color -NoNewline
Write-Host " $Message"
if ($Type -eq "HEAD") {
Write-Host ("=" * 60) -ForegroundColor Blue
}
}
function Write-OK { param([string]$M) Write-Status "OK" $M }
function Write-Warn { param([string]$M) Write-Status "WARN" $M }
function Write-Err { param([string]$M) Write-Status "ERR" $M }
function Write-Info { param([string]$M) Write-Status "INFO" $M }
function Write-Head { param([string]$M) Write-Host ""; Write-Status "HEAD" $M }
# =============================================================================
# SQL HELPERS
# =============================================================================
function New-SqlConnection {
param(
[string]$Server,
[string]$User,
[string]$Password,
[string]$Database = "master"
)
$connStr = "Server=$Server;Database=$Database;User Id=$User;Password=$Password;"
$connStr += "Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=30;"
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
return $conn
}
function Invoke-SqlQuery {
param(
[string]$Server,
[string]$User,
[string]$Password,
[string]$Query,
[string]$Database = "master",
[int]$Timeout = 30
)
$conn = New-SqlConnection -Server $Server -User $User -Password $Password -Database $Database
try {
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = $Query
$cmd.CommandTimeout = $Timeout
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
$dataset = New-Object System.Data.DataSet
$adapter.Fill($dataset) | Out-Null
$conn.Close()
return $dataset.Tables[0]
}
catch {
if ($conn.State -eq 'Open') { $conn.Close() }
throw $_
}
}
function Test-SqlConnection {
param(
[string]$Server,
[string]$User,
[string]$Password
)
try {
$result = Invoke-SqlQuery -Server $Server -User $User -Password $Password -Query "SELECT 1 AS Test"
return $true
}
catch {
return $false
}
}
# =============================================================================
# BACKUP FUNCTIONS
# =============================================================================
function Get-LatestBackupInfo {
param(
[string]$Server,
[string]$User,
[string]$Password,
[string]$Database
)
$query = @"
SELECT TOP 1
bmf.physical_device_name AS BackupPath,
CAST(bs.compressed_backup_size AS BIGINT) / 1024 / 1024 AS SizeMB,
CONVERT(VARCHAR(20), bs.backup_finish_date, 120) AS BackupDate,
bs.database_name AS DatabaseName
FROM msdb.dbo.backupset bs
INNER JOIN msdb.dbo.backupmediafamily bmf
ON bs.media_set_id = bmf.media_set_id
WHERE LOWER(bs.database_name) = LOWER('$Database')
AND bs.type = 'D'
ORDER BY bs.backup_finish_date DESC;
"@
$result = Invoke-SqlQuery -Server $Server -User $User -Password $Password -Query $query
if ($result -and $result.Rows.Count -gt 0) {
$row = $result.Rows[0]
return @{
Path = $row.BackupPath
SizeMB = [int]$row.SizeMB
Date = $row.BackupDate
Database = $row.DatabaseName
}
}
return $null
}
function Get-BackupFile {
param(
[string]$Server,
[string]$User,
[string]$Password,
[string]$RemotePath,
[string]$LocalPath
)
Write-Info "Downloading via OPENROWSET BULK..."
Write-Info " Source: $RemotePath"
Write-Info " Target: $LocalPath"
# Try UNC path direct copy first
if ($RemotePath.StartsWith("\\")) {
Write-Info "Attempting direct network copy..."
try {
Copy-Item -Path $RemotePath -Destination $LocalPath -Force -ErrorAction Stop
if (Test-Path $LocalPath) {
Write-OK "Downloaded via network share"
return $true
}
}
catch {
Write-Info "Network copy failed, using SQL bulk read..."
}
}
# Use SqlClient OPENROWSET
$conn = New-SqlConnection -Server $Server -User $User -Password $Password
try {
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = "SELECT BulkColumn FROM OPENROWSET(BULK '$RemotePath', SINGLE_BLOB) AS x"
$cmd.CommandTimeout = 3600 # 1 hour for large files
Write-Info "Reading backup (this may take several minutes)..."
$reader = $cmd.ExecuteReader()
if ($reader.Read()) {
$data = $reader.GetSqlBytes(0).Value
[System.IO.File]::WriteAllBytes($LocalPath, $data)
$sizeMB = [math]::Round($data.Length / 1MB, 1)
Write-OK "Downloaded $sizeMB MB"
$reader.Close()
$conn.Close()
return $true
}
else {
Write-Err "No data returned"
$reader.Close()
$conn.Close()
return $false
}
}
catch {
Write-Err "Download failed: $_"
if ($conn.State -eq 'Open') { $conn.Close() }
return $false
}
}
function Invoke-Backup {
Write-Head "Backup: $Database"
# Validate parameters
if (-not $SourceServer) { Write-Err "SourceServer required"; return $false }
if (-not $SourceUser) { Write-Err "SourceUser required"; return $false }
if (-not $SourcePassword) { Write-Err "SourcePassword required"; return $false }
if (-not $Database) { Write-Err "Database required"; return $false }
Write-Info "Source: $SourceServer"
Write-Info "Database: $Database"
# Test connection
Write-Info "Testing connection..."
if (-not (Test-SqlConnection -Server $SourceServer -User $SourceUser -Password $SourcePassword)) {
Write-Err "Cannot connect to source server"
return $false
}
Write-OK "Connected to source server"
# Create backup directory
if (-not (Test-Path $BackupDir)) {
New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null
Write-OK "Created backup directory: $BackupDir"
}
# Check for existing backup
$existingBackups = Get-ChildItem -Path $BackupDir -Filter "$Database*.bak" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($existingBackups -and -not $Force) {
$sizeMB = [math]::Round($existingBackups.Length / 1MB, 1)
Write-OK "Found existing: $($existingBackups.Name) ($sizeMB MB)"
Write-Info "Use -Force to re-download"
return $true
}
# Find latest backup on server
Write-Info "Querying backup history..."
$backupInfo = Get-LatestBackupInfo -Server $SourceServer -User $SourceUser -Password $SourcePassword -Database $Database
if (-not $backupInfo) {
Write-Err "No backup found for $Database"
return $false
}
$fileName = Split-Path $backupInfo.Path -Leaf
Write-OK "Found: $fileName"
Write-Info " Size: $($backupInfo.SizeMB) MB"
Write-Info " Date: $($backupInfo.Date)"
# Check size limit
if ($backupInfo.SizeMB -gt $MaxSizeMB) {
Write-Err "Backup too large: $($backupInfo.SizeMB) MB > $MaxSizeMB MB limit"
Write-Info "Use -MaxSizeMB to increase limit"
return $false
}
# Download
$localPath = Join-Path $BackupDir $fileName
return Get-BackupFile -Server $SourceServer -User $SourceUser -Password $SourcePassword `
-RemotePath $backupInfo.Path -LocalPath $localPath
}
# =============================================================================
# RESTORE FUNCTIONS
# =============================================================================
function Get-DefaultDataDir {
param(
[string]$Server,
[string]$User,
[string]$Password
)
$query = "SELECT SERVERPROPERTY('InstanceDefaultDataPath') AS DataPath"
try {
$result = Invoke-SqlQuery -Server $Server -User $User -Password $Password -Query $query
if ($result -and $result.Rows.Count -gt 0) {
return $result.Rows[0].DataPath.TrimEnd('\')
}
}
catch {}
return "C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA"
}
function Get-LogicalFileNames {
param(
[string]$Server,
[string]$User,
[string]$Password,
[string]$BackupPath
)
$query = "RESTORE FILELISTONLY FROM DISK = N'$BackupPath'"
$result = Invoke-SqlQuery -Server $Server -User $User -Password $Password -Query $query -Timeout 120
$dataFiles = @()
$logFile = $null
foreach ($row in $result.Rows) {
$name = $row.LogicalName
$type = $row.Type.ToString().ToUpper()
if ($type -eq "D") {
$dataFiles += $name
}
elseif ($type -eq "L" -and -not $logFile) {
$logFile = $name
}
}
return @{ Data = $dataFiles; Log = $logFile }
}
function Invoke-Restore {
$targetDb = if ($TargetDatabase) { $TargetDatabase } else { $Database }
Write-Head "Restore: $targetDb"
# Validate parameters
if (-not $TargetServer) { Write-Err "TargetServer required"; return $false }
if (-not $TargetUser) { Write-Err "TargetUser required"; return $false }
if (-not $TargetPassword) { Write-Err "TargetPassword required"; return $false }
if (-not $Database) { Write-Err "Database required"; return $false }
Write-Info "Target: $TargetServer"
Write-Info "Database: $targetDb"
# Test connection
Write-Info "Testing connection..."
if (-not (Test-SqlConnection -Server $TargetServer -User $TargetUser -Password $TargetPassword)) {
Write-Err "Cannot connect to target server"
return $false
}
Write-OK "Connected to target server"
# Find backup file
$backupPath = if ($BackupFile) {
if (Test-Path $BackupFile) {
(Resolve-Path $BackupFile).Path
} else {
Write-Err "Backup file not found: $BackupFile"
return $false
}
} else {
$backups = Get-ChildItem -Path $BackupDir -Filter "$Database*.bak" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $backups) {
Write-Err "No backup file found for $Database in $BackupDir"
return $false
}
$backups.FullName
}
$backupName = Split-Path $backupPath -Leaf
$sizeMB = [math]::Round((Get-Item $backupPath).Length / 1MB, 1)
Write-OK "Using: $backupName ($sizeMB MB)"
# Get data directory
$dataDirectory = if ($DataDir) { $DataDir } else {
Get-DefaultDataDir -Server $TargetServer -User $TargetUser -Password $TargetPassword
}
Write-Info "Data directory: $dataDirectory"
# Get logical file names
Write-Info "Reading backup structure..."
$logical = Get-LogicalFileNames -Server $TargetServer -User $TargetUser -Password $TargetPassword -BackupPath $backupPath
if (-not $logical -or $logical.Data.Count -eq 0 -or -not $logical.Log) {
Write-Err "Could not read logical file names"
return $false
}
Write-Info " Data files: $($logical.Data -join ', ')"
Write-Info " Log file: $($logical.Log)"
# Build MOVE clauses
$moveStmts = @()
$primary = $logical.Data[0]
$moveStmts += "MOVE N'$primary' TO N'$dataDirectory\$targetDb.mdf'"
for ($i = 1; $i -lt $logical.Data.Count; $i++) {
$df = $logical.Data[$i]
$suffix = ($df -split "_")[0]
$moveStmts += "MOVE N'$df' TO N'$dataDirectory\${targetDb}_${suffix}.ndf'"
}
$moveStmts += "MOVE N'$($logical.Log)' TO N'$dataDirectory\${targetDb}_log.ldf'"
# Drop existing database
Write-Info "Checking for existing database..."
$dropQuery = @"
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = '$targetDb')
BEGIN
ALTER DATABASE [$targetDb] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE [$targetDb];
END
"@
try {
Invoke-SqlQuery -Server $TargetServer -User $TargetUser -Password $TargetPassword -Query $dropQuery -Timeout 60
Write-OK "Cleared existing database"
}
catch {
Write-Warn "Could not drop existing database (may not exist)"
}
# Restore
Write-Info "Restoring database (this may take several minutes)..."
$moveClause = $moveStmts -join ", "
$restoreQuery = "RESTORE DATABASE [$targetDb] FROM DISK = N'$backupPath' WITH $moveClause, REPLACE, RECOVERY"
try {
Invoke-SqlQuery -Server $TargetServer -User $TargetUser -Password $TargetPassword -Query $restoreQuery -Timeout 1800
Write-OK "Database restored"
}
catch {
Write-Err "Restore failed: $_"
return $false
}
# Verify
Write-Info "Verifying..."
$verifyQuery = "SELECT name FROM sys.databases WHERE name = '$targetDb'"
$result = Invoke-SqlQuery -Server $TargetServer -User $TargetUser -Password $TargetPassword -Query $verifyQuery
if ($result -and $result.Rows.Count -gt 0) {
Write-OK "Database $targetDb is ready"
Write-Info ""
Write-Info "Connection details:"
Write-Info " Server: $TargetServer"
Write-Info " Database: $targetDb"
Write-Info " User: $TargetUser"
return $true
}
Write-Warn "Could not verify restore"
return $false
}
# =============================================================================
# INFO FUNCTION
# =============================================================================
function Invoke-Info {
Write-Head "Database Info"
$server = if ($SourceServer) { $SourceServer } else { $TargetServer }
$user = if ($SourceUser) { $SourceUser } else { $TargetUser }
$password = if ($SourcePassword) { $SourcePassword } else { $TargetPassword }
if (-not $server -or -not $user -or -not $password) {
Write-Err "Server credentials required"
return $false
}
Write-Info "Server: $server"
if (-not (Test-SqlConnection -Server $server -User $user -Password $password)) {
Write-Err "Cannot connect"
return $false
}
Write-OK "Connected"
if ($Database) {
# Show backup history for specific database
Write-Info "Backup history for: $Database"
$query = @"
SELECT TOP 5
CONVERT(VARCHAR(20), bs.backup_finish_date, 120) AS BackupDate,
CAST(bs.compressed_backup_size / 1024 / 1024 AS INT) AS SizeMB,
bmf.physical_device_name AS Path
FROM msdb.dbo.backupset bs
INNER JOIN msdb.dbo.backupmediafamily bmf ON bs.media_set_id = bmf.media_set_id
WHERE LOWER(bs.database_name) = LOWER('$Database') AND bs.type = 'D'
ORDER BY bs.backup_finish_date DESC;
"@
$result = Invoke-SqlQuery -Server $server -User $user -Password $password -Query $query
if ($result -and $result.Rows.Count -gt 0) {
Write-Host ""
$result | Format-Table -AutoSize
}
else {
Write-Warn "No backups found"
}
}
else {
# List all databases
Write-Info "Databases on server:"
$query = @"
SELECT
name AS DatabaseName,
CAST(SUM(size) * 8 / 1024 AS INT) AS SizeMB,
state_desc AS State
FROM sys.databases d
LEFT JOIN sys.master_files f ON d.database_id = f.database_id
WHERE d.database_id > 4
GROUP BY name, state_desc
ORDER BY name;
"@
$result = Invoke-SqlQuery -Server $server -User $user -Password $password -Query $query
if ($result -and $result.Rows.Count -gt 0) {
Write-Host ""
$result | Format-Table -AutoSize
}
else {
Write-Warn "No user databases found"
}
}
return $true
}
# =============================================================================
# MAIN
# =============================================================================
# Build database list from -Database or -Databases
$dbList = @()
if ($Databases) {
# Handle array or comma-separated string
foreach ($d in $Databases) {
$dbList += $d -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
}
}
if ($Database) {
$dbList += $Database -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
}
$dbList = $dbList | Select-Object -Unique
# Execute command
$successCount = 0
$failCount = 0
switch ($Command) {
"backup" {
if ($dbList.Count -eq 0) {
Write-Err "Database required. Use -Database or -Databases"
exit 1
}
foreach ($db in $dbList) {
$Database = $db
if (Invoke-Backup) { $successCount++ } else { $failCount++ }
}
}
"restore" {
if ($dbList.Count -eq 0) {
Write-Err "Database required. Use -Database or -Databases"
exit 1
}
foreach ($db in $dbList) {
$Database = $db
$TargetDatabase = $null # Reset for each db
if (Invoke-Restore) { $successCount++ } else { $failCount++ }
}
}
"info" {
if ($dbList.Count -eq 0) {
Invoke-Info
} else {
foreach ($db in $dbList) {
$Database = $db
Invoke-Info
}
}
}
}
# Summary for multiple databases
if ($dbList.Count -gt 1) {
Write-Head "Summary"
Write-OK "Completed: $successCount"
if ($failCount -gt 0) {
Write-Err "Failed: $failCount"
exit 1
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment