<# .SYNOPSIS Guided setup for the MonitorSpider Windows backup kit - paste your monitor's ping URL, answer a few questions, and it does the rest. .DESCRIPTION Unzip the kit ANYWHERE (Downloads is fine) and DOUBLE-CLICK START-HERE.cmd - it launches this script past Windows' downloaded-file script block (execution policy / mark-of-the-web); approve the administrator prompt when asked. The wizard will: 0. Relaunch itself elevated if needed, and copy the kit to its permanent home (C:\server-backups\kit) so you can delete the unzipped folder. 1. Ask for your monitor's ping URL (shown on the monitor's page at monitorspider.com) and fetch the monitor's settings to propose a matching schedule. 2. Detect your SQL Server instances and ask what to back up and where to upload it, then write backup-config.json for you. 3. Download any missing kit files, then run preflight.ps1 -Install to fetch the portable tools (7-Zip, WinSCP assembly, sqlcmd - SHA-256 verified, nothing installed system-wide) and verify everything works end to end. 4. Register the scheduled task (runs as SYSTEM) that executes the backup. 5. Offer to run the first backup right away. Re-running is safe: it re-uses the existing config as defaults and re-registers the task in place. .NOTES From https://monitorspider.com/backup-kits/ - MIT licensed. All files are plain text; read them before you run them. #> [CmdletBinding()] param( [string]$PingUrl = '' ) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $installDir = 'C:\server-backups\kit' # the kit's permanent home; unzip location is temporary # --- self-elevate: so plain right-click -> "Run with PowerShell" just works --- # (installing tools is user-level, but the SYSTEM scheduled task needs admin) $winPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $winPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host 'Requesting administrator rights (needed to schedule the backup task)...' $relaunch = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-NoExit', '-File', "`"$PSCommandPath`"") if ($PingUrl) { $relaunch += @('-PingUrl', "`"$PingUrl`"") } Start-Process powershell.exe -Verb RunAs -ArgumentList $relaunch exit } # --- self-install: copy the kit to its permanent home, continue from there --- # The scheduled task will point at $installDir, so setup must not run the backup # out of a Downloads folder someone later deletes. $here = [IO.Path]::GetFullPath($PSScriptRoot).TrimEnd('\') if ($here -ne [IO.Path]::GetFullPath($installDir).TrimEnd('\')) { New-Item -ItemType Directory -Force -Path $installDir | Out-Null foreach ($f in @('START-HERE.cmd', 'setup.ps1', 'preflight.ps1', 'windows-sqlserver.ps1', 'windows-backup-config.example.json', 'Send-BackupPing.ps1')) { $src = Join-Path $here $f if (Test-Path $src) { Copy-Item -Path $src -Destination (Join-Path $installDir $f) -Force Unblock-File -Path (Join-Path $installDir $f) -ErrorAction SilentlyContinue } } Write-Host "Kit installed to $installDir - you can delete the unzipped folder afterwards." -ForegroundColor Green Write-Host '' & (Join-Path $installDir 'setup.ps1') -PingUrl $PingUrl exit $LASTEXITCODE } $kitDir = $PSScriptRoot $configPath = Join-Path $kitDir 'backup-config.json' $taskName = 'MonitorSpider Backup' function Ask { param([string]$Prompt, [string]$Default = '') $suffix = if ($Default -ne '') { " [$Default]" } else { '' } $answer = Read-Host "$Prompt$suffix" if ($answer -eq '') { return $Default } return $answer } Write-Host '' Write-Host '=== MonitorSpider backup setup ===' -ForegroundColor Cyan Write-Host '' # --- 1. monitor: ping URL -> fetch settings --------------------------------- Write-Host 'Step 1 of 4 - your MonitorSpider monitor' -ForegroundColor Cyan Write-Host 'In the dashboard: Monitors -> your Backup (push) monitor -> copy the URL from the' Write-Host '"Backup ping endpoint" card. (No monitor yet? Create one at monitorspider.com -' Write-Host 'the free plan includes one backup monitor.)' while ($true) { if (-not $PingUrl) { $PingUrl = Read-Host 'Paste the ping URL' } if ($PingUrl -match 'id=(\d+).*?token=([0-9a-fA-F]+)') { $monId = $Matches[1]; $monToken = $Matches[2] $baseUrl = ([uri]$PingUrl).GetLeftPart([UriPartial]::Authority) break } Write-Host 'That does not look like a ping URL (expected ...backup-ping?id=&token=).' -ForegroundColor Yellow $PingUrl = '' } try { $mon = Invoke-RestMethod -Uri "$baseUrl/srv/backup-config?id=$monId&token=$monToken" -TimeoutSec 20 } catch { Write-Host "Could not fetch the monitor's settings ($_)." -ForegroundColor Red Write-Host 'Check the URL is complete and the monitor is enabled, then run setup again.' exit 1 } Write-Host ("Connected: monitor '{0}' expects a backup every {1}h (+{2}h grace)." -f $mon.name, $mon.expected_interval_hours, $mon.grace_hours) -ForegroundColor Green Write-Host '' # --- 2. backup questions -> backup-config.json ------------------------------ Write-Host 'Step 2 of 4 - what to back up, where to send it' -ForegroundColor Cyan $existing = $null if (Test-Path $configPath) { Write-Host "Found an existing $configPath - its values are offered as defaults." try { $existing = Get-Content -Raw $configPath | ConvertFrom-Json } catch { $existing = $null } } function OldVal { param([string]$Name, $Default) if ($existing -and $existing.PSObject.Properties[$Name] -and $null -ne $existing.$Name -and "$($existing.$Name)" -ne '') { return $existing.$Name } return $Default } # SQL instances from the registry; the customer just picks a number. $sqlChoice = '' $instances = @() $reg = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' -ErrorAction SilentlyContinue if ($reg) { $instances = @($reg.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object { $_.Name }) } if ($instances.Count -gt 0) { Write-Host 'SQL Server instances found on this machine:' for ($i = 0; $i -lt $instances.Count; $i++) { $conn = if ($instances[$i] -eq 'MSSQLSERVER') { '.' } else { ".\$($instances[$i])" } Write-Host (" {0}) {1} (connect as {2})" -f ($i + 1), $instances[$i], $conn) } Write-Host ' 0) none - files-only backup' $pick = Ask 'Back up which instance?' '1' if ($pick -match '^\d+$' -and [int]$pick -ge 1 -and [int]$pick -le $instances.Count) { $name = $instances[[int]$pick - 1] $sqlChoice = if ($name -eq 'MSSQLSERVER') { '.' } else { ".\$name" } } } else { Write-Host 'No SQL Server instances detected - configuring a files-only backup.' } $foldersRaw = Ask 'Folders to back up (comma-separated)' ((OldVal 'Folders' @($(if (Test-Path 'C:\inetpub\wwwroot') { 'C:\inetpub\wwwroot' }))) -join ', ') $folders = @($foldersRaw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) foreach ($f in $folders) { if (-not (Test-Path $f)) { Write-Host " note: $f does not exist right now" -ForegroundColor Yellow } } if (-not $sqlChoice -and $folders.Count -eq 0) { Write-Host 'Nothing to back up - pick an instance or at least one folder.' -ForegroundColor Red; exit 1 } $oldUp = if ($existing) { $existing.Upload } else { $null } Write-Host '' Write-Host 'Where should the encrypted backups be uploaded? (any FTP/FTPS/SFTP account works -' Write-Host 'shared-hosting FTP space, a NAS, another server...)' $proto = '' while ($proto -notin @('Ftp', 'Sftp')) { $proto = (Ask 'Protocol: Ftp or Sftp' ($(if ($oldUp) { $oldUp.Protocol } else { 'Ftp' }))) } $upHost = '' while (-not $upHost) { $upHost = Ask 'Upload host (e.g. backups.example.com)' ($(if ($oldUp) { $oldUp.HostName } else { '' })) } $upUser = '' while (-not $upUser) { $upUser = Ask 'Upload username' ($(if ($oldUp) { $oldUp.UserName } else { '' })) } $upPass = '' while (-not $upPass) { $upPass = Ask 'Upload password' ($(if ($oldUp) { $oldUp.Password } else { '' })) } $upDir = Ask 'Remote folder' ($(if ($oldUp) { $oldUp.RemoteDir } else { '/' })) $ftpSecure = 'Explicit'; $tlsFp = ''; $sshFp = '' if ($proto -eq 'Ftp') { $ftpSecure = Ask 'FTP encryption: Explicit (FTPS, recommended), Implicit, or None' ($(if ($oldUp -and $oldUp.PSObject.Properties['FtpSecure']) { $oldUp.FtpSecure } else { 'Explicit' })) if ($ftpSecure -ne 'None') { $tlsFp = Ask 'TLS certificate fingerprint (Enter to skip; only needed if the host cert does not match its name - WinSCP GUI shows it)' ($(if ($oldUp -and $oldUp.PSObject.Properties['TlsCertificateFingerprint']) { $oldUp.TlsCertificateFingerprint } else { '' })) } } else { $sshFp = Ask 'SSH host key fingerprint (required for SFTP; the WinSCP GUI shows it on first connect)' ($(if ($oldUp -and $oldUp.PSObject.Properties['SshHostKeyFingerprint']) { $oldUp.SshHostKeyFingerprint } else { '' })) if (-not $sshFp) { Write-Host ' note: SFTP will not connect without it - you can add it to backup-config.json later.' -ForegroundColor Yellow } } Write-Host '' $archPass = OldVal 'ArchivePassword' '' if ($archPass -and $archPass -notmatch '^<.*>$') { Write-Host 'Keeping the existing archive password from backup-config.json.' } else { $archPass = Ask 'Archive password - encrypts the backups. Enter your own, or press Enter to generate one' if (-not $archPass) { $archPass = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 24 | ForEach-Object { [char]$_ }) Write-Host '' Write-Host " Generated archive password: $archPass" -ForegroundColor Yellow Write-Host ' SAVE IT IN YOUR PASSWORD MANAGER NOW - without it the backups CANNOT be' -ForegroundColor Yellow Write-Host ' restored, and it must not live only on this server.' -ForegroundColor Yellow Read-Host ' Press Enter once you have saved it' } } $prefix = Ask 'Backup set name prefix' (OldVal 'SetPrefix' (($mon.name -replace '[^a-zA-Z0-9-]', '-' -replace '-+', '-').Trim('-').ToLower())) $staging = Ask 'Local staging folder' (OldVal 'StagingDir' 'C:\server-backups') $config = [ordered]@{ SetPrefix = $prefix SqlInstance = $sqlChoice ExcludeDatabases = @(OldVal 'ExcludeDatabases' @()) Folders = $folders StagingDir = $staging ArchivePassword = $archPass VolumeSizeMB = [int](OldVal 'VolumeSizeMB' 500) Upload = [ordered]@{ Protocol = $proto; HostName = $upHost; UserName = $upUser; Password = $upPass RemoteDir = $upDir; FtpSecure = $ftpSecure TlsCertificateFingerprint = $tlsFp; SshHostKeyFingerprint = $sshFp } Retention = if ($existing -and $existing.PSObject.Properties['Retention']) { $existing.Retention } else { [ordered]@{ WeeklyDays = 28; MonthlyDays = 180; KeepLocalSets = 1 } } MonitorSpider = [ordered]@{ PingUrl = $PingUrl } } $config | ConvertTo-Json -Depth 5 | Set-Content -Path $configPath -Encoding UTF8 Write-Host "Wrote $configPath" -ForegroundColor Green Write-Host '' # --- 3. kit files + tools + preflight --------------------------------------- Write-Host 'Step 3 of 4 - tools and pre-flight checks' -ForegroundColor Cyan foreach ($f in @('windows-sqlserver.ps1', 'preflight.ps1')) { $p = Join-Path $kitDir $f if (-not (Test-Path $p)) { Write-Host "Downloading missing $f from $baseUrl/backup-kits/ ..." Invoke-WebRequest -Uri "$baseUrl/backup-kits/$f" -OutFile $p -UseBasicParsing } } & (Join-Path $kitDir 'preflight.ps1') -Install -ConfigPath $configPath if ($LASTEXITCODE -ne 0) { Write-Host '' Write-Host 'Pre-flight found problems (see above). Fix them and run setup again -' -ForegroundColor Red Write-Host 'your answers are saved in backup-config.json and will be offered as defaults.' -ForegroundColor Red exit 1 } Write-Host '' # --- 4. scheduled task ------------------------------------------------------- Write-Host 'Step 4 of 4 - schedule it' -ForegroundColor Cyan $daily = ($mon.expected_interval_hours -le 24) $time = Ask 'Run at what time? (24h HH:mm)' '02:00' if ($daily) { Write-Host "Your monitor expects a backup every $($mon.expected_interval_hours)h - scheduling DAILY at $time." $trigger = New-ScheduledTaskTrigger -Daily -At $time } else { $day = Ask 'Run on which day? (Monday..Sunday)' 'Sunday' $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $day -At $time Write-Host "Scheduling WEEKLY on $day at $time (monitor window: $($mon.expected_interval_hours)h + $($mon.grace_hours)h grace)." } $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$(Join-Path $kitDir 'windows-sqlserver.ps1')`"" $taskPrincipal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 8) Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` -Principal $taskPrincipal -Settings $settings -Force | Out-Null Write-Host "Registered scheduled task '$taskName' (runs as SYSTEM)." -ForegroundColor Green Write-Host '' Write-Host 'Setup complete.' -ForegroundColor Green Write-Host " - Config: $configPath" Write-Host " - Task: $taskName (test later with: Start-ScheduledTask -TaskName '$taskName')" Write-Host " - Monitor: $($mon.name) - it will alert if a backup fails or stops arriving." Write-Host '' $runNow = Ask 'Run the first backup now? It may take a while, but it seeds the monitor and the size graph (y/N)' 'N' if ($runNow -match '^[yY]') { & (Join-Path $kitDir 'windows-sqlserver.ps1') -ConfigPath $configPath Write-Host '' if ($LASTEXITCODE -eq 0) { Write-Host 'First backup completed - check the monitor page: it should be green with a size sample.' -ForegroundColor Green } else { Write-Host 'First backup reported errors (see the log above) - the monitor received a FAIL ping, which is the system working.' -ForegroundColor Yellow } }