#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" $HostLabel = 'RUVDS' $SourceDisplay = "RUVDS ($env:COMPUTERNAME / 80.64.31.36)" $DestDisplay = "kreknin:/volume1/NetBackup/ruvds-iis/$today/" function Format-Duration([int]$sec) { '{0}m{1:D2}s' -f ([int]($sec / 60)), ($sec % 60) } if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } Start-Transcript -Path $logFile -Append -Force | Out-Null $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 { Write-Host " ntfy WARNING: $($_.Exception.Message)" -ForegroundColor Yellow } } function Notify-Email($subject, $body) { try { if (-not $cfg.SMTP_HOST -or -not $cfg.SMTP_USER -or -not $cfg.SMTP_PASS -or -not $cfg.OPS_NOTIFY_EMAIL) { return } $secpass = ConvertTo-SecureString $cfg.SMTP_PASS -AsPlainText -Force $mailCred = New-Object PSCredential($cfg.SMTP_USER, $secpass) $prevEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { Send-MailMessage -SmtpServer $cfg.SMTP_HOST -Port ([int]$cfg.SMTP_PORT) -UseSsl ` -Credential $mailCred ` -From $cfg.SMTP_FROM -To $cfg.OPS_NOTIFY_EMAIL ` -Subject $subject -Body $body -Encoding UTF8 ` -WarningAction SilentlyContinue } finally { $ErrorActionPreference = $prevEAP } } catch { Write-Host " email WARNING: $($_.Exception.Message)" -ForegroundColor Yellow } } function Invoke-Rclone { param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Args) $prevEAP = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { return & $rcloneExe @Args 2>&1 } 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 $durationSec = [int](New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds $durationHuman = Format-Duration $durationSec $srcBytes = (Get-ChildItem 'C:\sites\snolla' -Recurse -File -EA SilentlyContinue | Measure-Object Length -Sum).Sum $sizeStr = '{0:N2} GB' -f ($srcBytes / 1GB) $snapshotCount = if ($existing) { @($existing).Count } else { 1 } # ntfy push (single line) $ntfyBody = "$durationHuman, size=$sizeStr, snapshots=$snapshotCount, dest=$DestDisplay, certs=$exported" Notify-Ntfy "$HostLabel backup OK $today" $ntfyBody 'default' 'green_circle' # email (structured) $emailBody = @" $HostLabel daily backup completed successfully. Date: $today Duration: $durationHuman Size: $sizeStr Snapshots: $snapshotCount Source: $SourceDisplay Dest: $DestDisplay Components: - sites/snolla - iis-config (applicationHost.config) - iis-backup-webconfiguration - certs ($exported PFX exports) - ssh-config Log: $logFile "@ Notify-Email "[$HostLabel] backup OK $today" $emailBody Write-Host "`n=== DONE in $durationHuman ===" } catch { $err = $_.Exception.Message Write-Host "`n=== FAILED: $err ===" -ForegroundColor Red Write-Host $_.ScriptStackTrace $durationSec = [int](New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds $durationHuman = Format-Duration $durationSec $tailLog = '' try { $tailLog = (Get-Content $logFile -Tail 40 -EA SilentlyContinue) -join "`n" } catch {} # ntfy push (single line) Notify-Ntfy "$HostLabel backup FAILED $today" "After $durationHuman`: $err. See $logFile" 'high' 'red_circle' # email (structured) $emailBody = @" $HostLabel daily backup FAILED. Date: $today Duration: $durationHuman Error: $err Source: $SourceDisplay Log: $logFile Tail (last 40 lines): $tailLog "@ Notify-Email "[$HostLabel] backup FAILED $today" $emailBody Stop-Transcript | Out-Null exit 1 } finally { try { Stop-Transcript | Out-Null } catch {} }