# 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.' }