#requires -Version 5.1 [CmdletBinding()] param() $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $today = Get-Date -Format 'yyyy-MM-dd' $start = Get-Date $base = 'C:\ProgramData\backup' $logDir = "$base\logs" $logFile = "$logDir\$today.log" $rcloneExe = 'C:\Program Files\rclone\rclone.exe' $rcloneCfg = "$base\rclone.conf" $remoteBase = "kreknin:NetBackup/ruvds-iis" $remoteToday = "$remoteBase/$today" if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } Start-Transcript -Path $logFile -Append -Force | Out-Null # Load ntfy creds $cfg = @{} if (Test-Path "$base\config.env") { Get-Content "$base\config.env" | Where-Object { $_ -match '^[A-Z_]+=' } | ForEach-Object { $kv = $_ -split '=', 2 $cfg[$kv[0]] = $kv[1] } } function Notify-Ntfy($title, $msg, $priority='default', $tags='') { try { if (-not $cfg.NTFY_URL -or -not $cfg.NTFY_USER -or -not $cfg.NTFY_PASS) { return } $pair = "$($cfg.NTFY_USER):$($cfg.NTFY_PASS)" $auth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair)) $topic = if ($cfg.NTFY_TOPIC) { $cfg.NTFY_TOPIC } else { 'vds-backup' } Invoke-RestMethod -Uri "$($cfg.NTFY_URL)/$topic" -Method POST ` -Headers @{ Authorization=$auth; Title=$title; Priority=$priority; Tags=$tags } ` -Body $msg -ContentType 'text/plain' -ErrorAction SilentlyContinue | Out-Null } catch {} } # Invoke rclone tolerating its NOTICE stderr (PS Stop catches them otherwise). # Returns array of stdout lines + non-zero $LASTEXITCODE on real failure. function Invoke-Rclone { param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Args) $prevEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $out = & $rcloneExe @Args 2>&1 return $out } finally { $ErrorActionPreference = $prevEAP } } try { Write-Host "=== RUVDS backup $today started at $start ===" Write-Host "`n--- 1. IIS config snapshot ---" Import-Module WebAdministration $iisBackupName = "daily-$today" $iisBackupDir = "$env:SystemRoot\System32\inetsrv\backup\$iisBackupName" if (Get-WebConfigurationBackup -Name $iisBackupName -ErrorAction SilentlyContinue) { Remove-WebConfigurationBackup -Name $iisBackupName } if (Test-Path $iisBackupDir) { Remove-Item $iisBackupDir -Recurse -Force } Backup-WebConfiguration -Name $iisBackupName | Out-Null Write-Host " $iisBackupDir created" Write-Host "`n--- 2. Cert store export ---" $certDir = "$base\certs-$today" if (Test-Path $certDir) { Remove-Item $certDir -Recurse -Force } New-Item -ItemType Directory -Path $certDir -Force | Out-Null $pfxPass = ConvertTo-SecureString 'ruvds-backup-pfx' -AsPlainText -Force $exported = 0 Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.HasPrivateKey } | ForEach-Object { $cn = ($_.Subject -split ',')[0] -replace 'CN=','' -replace '[^A-Za-z0-9.-]','_' if ($cn.Length -gt 60) { $cn = $cn.Substring(0,60) } try { Export-PfxCertificate -Cert $_ -FilePath "$certDir\$cn-$($_.Thumbprint.Substring(0,8)).pfx" -Password $pfxPass -ErrorAction SilentlyContinue | Out-Null $exported++ } catch {} } Write-Host " exported $exported certs" Write-Host "`n--- 3. rclone sync ---" $rcCommon = @('--config', $rcloneCfg, '--transfers', '4', '--checkers', '8', '--stats=0') Invoke-Rclone sync C:\sites\snolla "$remoteToday/sites/snolla/" @rcCommon | Out-Null if ($LASTEXITCODE -ne 0) { throw "rclone sync snolla failed (exit $LASTEXITCODE)" } Write-Host " snolla synced" Invoke-Rclone copy 'C:\Windows\System32\inetsrv\config\applicationHost.config' "$remoteToday/iis-config/" @rcCommon | Out-Null if ($LASTEXITCODE -ne 0) { throw "rclone copy applicationHost.config failed (exit $LASTEXITCODE)" } Write-Host " applicationHost.config copied" Invoke-Rclone sync $iisBackupDir "$remoteToday/iis-backup-webconfiguration/" @rcCommon | Out-Null if ($LASTEXITCODE -ne 0) { throw "rclone sync iis-backup-webconfiguration failed (exit $LASTEXITCODE)" } Write-Host " iis-backup-webconfiguration synced" Invoke-Rclone sync $certDir "$remoteToday/certs/" @rcCommon | Out-Null if ($LASTEXITCODE -ne 0) { throw "rclone sync certs failed (exit $LASTEXITCODE)" } Write-Host " certs synced" Invoke-Rclone sync C:\ProgramData\ssh "$remoteToday/ssh-config/" @rcCommon | Out-Null if ($LASTEXITCODE -ne 0) { throw "rclone sync ssh-config failed (exit $LASTEXITCODE)" } Write-Host " ssh-config synced" Write-Host "`n--- 4. Retention prune (keep last 7) ---" try { $lsdOut = Invoke-Rclone lsd $remoteBase --config $rcloneCfg $existing = $lsdOut | ForEach-Object { $line = "$_".Trim() if ($line -match '\s(\d{4}-\d{2}-\d{2})\s*$') { $Matches[1] } } | Sort-Object -Unique $toPrune = @($existing | Select-Object -SkipLast 7) foreach ($d in $toPrune) { Write-Host " pruning $d" Invoke-Rclone purge "$remoteBase/$d" --config $rcloneCfg | Out-Null } Write-Host " kept $([math]::Min(@($existing).Count, 7)) snapshots; pruned $(@($toPrune).Count)" } catch { Write-Host " retention prune WARNING: $($_.Exception.Message)" -ForegroundColor Yellow } Remove-Item $certDir -Recurse -Force -ErrorAction SilentlyContinue $duration = [int](New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds $msg = "RUVDS daily backup $today OK | $duration sec | $exported certs | snolla + IIS configs + ssh state" Notify-Ntfy "RUVDS backup OK ($today)" $msg 'default' 'green_circle' Write-Host "`n=== DONE in $duration sec ===" } catch { $err = $_.Exception.Message Write-Host "`n=== FAILED: $err ===" -ForegroundColor Red Write-Host $_.ScriptStackTrace $duration = [int](New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds Notify-Ntfy "RUVDS backup FAILED ($today)" "After $duration sec: $err" 'high' 'red_circle' Stop-Transcript | Out-Null exit 1 } finally { try { Stop-Transcript | Out-Null } catch {} }