diff --git a/tools/heim-abholen.ps1 b/tools/heim-abholen.ps1 new file mode 100644 index 0000000..36bd92f --- /dev/null +++ b/tools/heim-abholen.ps1 @@ -0,0 +1,187 @@ +# Holt die Sicherungen aus dem synchronisierten Nextcloud-Ordner in eine +# EIGENE Ablage — und bricht damit die Rückkopplung. +# +# LÄUFT AUF DEM HEIMRECHNER, nicht auf dem Server. +# +# pwsh -File heim-abholen.ps1 -Quelle "D:\Nextcloud\Sicherungen\d4rk_media" -Ziel "D:\Archiv\d4rk_media" +# pwsh -File heim-abholen.ps1 ... -Einrichten <- als tägliche Aufgabe eintragen +# pwsh -File heim-abholen.ps1 ... -Webhook "https://discord.com/api/webhooks/..." +# +# WOZU, wo der Nextcloud-Client doch schon synchronisiert. +# +# WEIL ER IN BEIDE RICHTUNGEN ABGLEICHT. Verschwinden die Archive auf dem +# Server — durch einen Verschlüsselungstrojaner, einen Fehlgriff, einen +# kaputten Auftrag —, löscht der Client sie auch hier. Der synchronisierte +# Ordner ist deshalb eine Kopie, aber keine Sicherung. +# +# Dieses Skript kopiert daraus in einen zweiten Ordner, den der Client NICHT +# kennt. Was einmal hier liegt, bleibt liegen, bis die Aufbewahrungsfrist es +# wegräumt. Damit ist die Kette gebrochen: der Server kann diese Kopien nicht +# mehr erreichen. +# +# UND ES MELDET SICH, WENN NICHTS MEHR ANKOMMT. Ein Kopierauftrag, der still +# aufhört zu laufen, ist genau der Schaden, gegen den er gebaut wurde: man +# glaubt, man hätte eine Sicherung, und merkt es an dem Tag, an dem man sie +# braucht. Deshalb prüft er das Alter des neuesten Archivs und schlägt Alarm. + +[CmdletBinding()] +param( + # Der Ordner, den der Nextcloud-Client synchronisiert. + [Parameter(Mandatory = $true)][string]$Quelle, + # Die eigene Ablage. MUSS ausserhalb des synchronisierten Ordners liegen -- + # sonst schiebt der Client die Kopien wieder zum Server hoch, und die + # Rueckkopplung, die wir brechen wollen, ist wieder da. + [Parameter(Mandatory = $true)][string]$Ziel, + [int]$Behalten = 30, + # Ab wann "es kommt nichts mehr" gemeldet wird. Die Sicherung laeuft + # taeglich; zwei Tage lassen Raum fuer einen ausgefallenen Lauf, drei waeren + # schon zwei verpasste. + [int]$AlarmNachTagen = 2, + [string]$Webhook = '', + [switch]$Einrichten +) + +$ErrorActionPreference = 'Stop' +function Schritt($t) { Write-Host "==> $t" -ForegroundColor Cyan } +function Gut($t) { Write-Host " $t" -ForegroundColor Green } +function Warn($t) { Write-Host " $t" -ForegroundColor Yellow } +function Schlecht($t) { Write-Host " $t" -ForegroundColor Red } + +$MUSTER = @('*.tar.gz', '*.db.gz') + +# -- Als Aufgabe eintragen ------------------------------------------------------ + +if ($Einrichten) { + $pwsh = (Get-Command pwsh.exe -ErrorAction SilentlyContinue).Source + if (-not $pwsh) { $pwsh = (Get-Command powershell.exe).Source } + $arg = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$PSCommandPath`"" + + " -Quelle `"$Quelle`" -Ziel `"$Ziel`" -Behalten $Behalten -AlarmNachTagen $AlarmNachTagen" + if ($Webhook) { $arg += " -Webhook `"$Webhook`"" } + + # Ueber einen VBS-Starter, damit kein Fenster aufgeht -- wie beim Tunnel. + # Ein Fenster, das man zumachen kann, wird irgendwann zugemacht. + $ordner = Join-Path $env:LOCALAPPDATA 'Sicherungs-Abholung' + New-Item -ItemType Directory -Force -Path $ordner | Out-Null + $starter = Join-Path $ordner 'start.vbs' + # Die Anfuehrungszeichen im Argumenttext muessen fuer VBS verdoppelt werden. + # Als Here-String, weil die verschachtelte Variante beim ersten Anlauf ein + # unlesbares Knaeuel war -- und prompt nicht geparst hat. + $vbsArg = $arg -replace '"', '""' + @" +' Erzeugt von heim-abholen.ps1 -- startet die Abholung OHNE Fenster. +Set sh = CreateObject("WScript.Shell") +sh.Run """$pwsh"" $vbsArg", 0, False +"@ | Set-Content $starter -Encoding ASCII + + Unregister-ScheduledTask -TaskName 'Sicherungen abholen' -Confirm:$false -ErrorAction SilentlyContinue + $aktion = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\wscript.exe" -Argument "`"$starter`"" + # ZWEI Ausloeser: taeglich mittags, UND beim Anmelden. Der zweite ist der + # wichtige -- ein Rechner, der nachts aus ist, holt sonst nie etwas. + $t1 = New-ScheduledTaskTrigger -Daily -At 12:00 + $t2 = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME" + $prinzipal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited + $optionen = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries + Register-ScheduledTask -TaskName 'Sicherungen abholen' -Action $aktion -Trigger @($t1, $t2) ` + -Principal $prinzipal -Settings $optionen ` + -Description "Kopiert Sicherungen aus $Quelle nach $Ziel und haelt $Behalten Staende." | Out-Null + Gut 'Als Aufgabe eingetragen: taeglich 12:00 und bei jeder Anmeldung.' +} + +# -- Prüfen --------------------------------------------------------------------- + +Schritt 'Nachsehen' +if (-not (Test-Path $Quelle)) { + Schlecht "Die Quelle gibt es nicht: $Quelle" + Schlecht 'Synchronisiert der Nextcloud-Client diesen Ordner wirklich?' + exit 1 +} +# Das Ziel darf NICHT im synchronisierten Ordner liegen -- sonst laedt der +# Client die Kopien wieder hoch und die Rueckkopplung ist zurueck. +$qVoll = (Resolve-Path $Quelle).Path.TrimEnd('\') +New-Item -ItemType Directory -Force -Path $Ziel | Out-Null +$zVoll = (Resolve-Path $Ziel).Path.TrimEnd('\') +if ($zVoll.StartsWith($qVoll, [StringComparison]::OrdinalIgnoreCase)) { + Schlecht 'Das Ziel liegt IM synchronisierten Ordner. Dann waere alles umsonst:' + Schlecht 'der Client wuerde die Kopien wieder hochladen und mitloeschen.' + exit 1 +} +Gut "aus $qVoll" +Gut "nach $zVoll" + +# -- Holen ---------------------------------------------------------------------- +# +# robocopy OHNE /MIR, und das ist der ganze Punkt: /MIR wuerde das Ziel dem +# Quellordner angleichen und dort geloeschte Dateien auch hier loeschen. Genau +# das soll nicht passieren. + +Schritt 'Kopieren (nur neue, nichts loeschen)' +$zaehlen = { @(Get-ChildItem $zVoll -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '\.(tar|db)\.gz$' }).Count } +$vorher = & $zaehlen +robocopy $qVoll $zVoll @MUSTER /XO /R:2 /W:5 /NP /NJH /NJS /NDL /NC /NS | Out-Null +# robocopy meldet ueber den Rueckgabewert: alles unter 8 ist Erfolg. +if ($LASTEXITCODE -ge 8) { Schlecht "robocopy meldet Fehler ($LASTEXITCODE)"; exit 1 } +$nachher = & $zaehlen +Gut "$($nachher - $vorher) neu, $nachher insgesamt" + +# -- Ausdünnen ------------------------------------------------------------------ + +$alle = @(Get-ChildItem $zVoll -File | Where-Object { $_.Name -match '\.(tar|db)\.gz$' } | + Sort-Object LastWriteTime -Descending) +if ($alle.Count -gt $Behalten) { + Schritt "Alte Staende entfernen (mehr als $Behalten)" + $alle | Select-Object -Skip $Behalten | ForEach-Object { + Remove-Item $_.FullName -Force + Gut "entfernt: $($_.Name)" + } +} + +# -- Kommt überhaupt noch etwas an? --------------------------------------------- + +Schritt 'Alter der neuesten Sicherung' +$neuestes = $alle | Select-Object -First 1 +if (-not $neuestes) { + $text = 'In der Ablage liegt KEINE einzige Sicherung.' + Schlecht $text +} else { + $tage = [math]::Round(((Get-Date) - $neuestes.LastWriteTime).TotalDays, 1) + if ($tage -gt $AlarmNachTagen) { + $text = "Die neueste Sicherung ist $tage Tage alt ($($neuestes.Name)). Es kommt nichts mehr an." + Schlecht $text + } else { + $text = $null + Gut "$($neuestes.Name), $tage Tage alt, $([math]::Round($neuestes.Length/1MB)) MB" + } +} + +# -- Melden, falls gewünscht ---------------------------------------------------- +# +# Nur im Alarmfall und nur bei ZUSTANDSWECHSEL. Eine tägliche Meldung "alles in +# Ordnung" liest nach einer Woche niemand mehr, und dann auch die eine nicht, +# auf die es ankommt. + +$merker = Join-Path $zVoll '.letzter-alarm.txt' +$zuletzt = if (Test-Path $merker) { (Get-Content $merker -Raw).Trim() } else { '' } +$jetzt = if ($text) { $text } else { '' } + +if ($jetzt -ne $zuletzt) { + Set-Content $merker $jetzt -Encoding UTF8 + if ($Webhook -and $jetzt) { + $rumpf = @{ + username = 'Sicherungs-Abholung' + embeds = @(@{ + title = 'Bei dir zu Hause kommt keine Sicherung mehr an' + description = $jetzt + color = 15158332 + fields = @(@{ name = 'Ablage'; value = "``$zVoll``"; inline = $false }) + footer = @{ text = 'D4RKST3R // ABHOLUNG' } + }) + } | ConvertTo-Json -Depth 6 + $code = $rumpf | curl.exe -s -o NUL -w '%{http_code}' -X POST ` + -H 'content-type: application/json' --data-binary '@-' $Webhook + Write-Host '' + if ($code -in '200', '204') { Gut "Gemeldet (HTTP $code)." } else { Schlecht "Melden fehlgeschlagen: HTTP $code" } + } +} elseif ($jetzt) { + Warn 'Schon gemeldet -- bleibt still, bis sich etwas aendert.' +}