diff --git a/scripts/update.ps1 b/scripts/update.ps1 index f53e44d..c172ebc 100644 --- a/scripts/update.ps1 +++ b/scripts/update.ps1 @@ -2,24 +2,77 @@ # Zentraler Updater - wird von update.bat per Invoke-Expression geladen. # Erwartet im aufrufenden Scope: # $proj - Projektname (z.B. 'VI3DGL', 'FLD-Schichtplanung', 'DRIVE', 'Portal_Union') -# $root - Installationspfad (Verzeichnis mit server.ps1, VERSION usw.) -# $stopBat - (optional) Dateiname des Stop-Skripts, z.B. 'DRIVE_Stop.bat'. -# Leerstring '' = kein Stop-Schritt. Nicht gesetzt = Fallback auf 'stop.bat'. -# $startBat - (optional) Dateiname des Start-Skripts, z.B. 'DRIVE_Start.bat'. -# Nicht gesetzt = Fallback auf 'dgl.bat'. +# $root - Installationspfad (Portal-Wurzel mit index.html, VERSION usw.) +# $stopBat - (optional) Stop-Skript-Dateiname; ''=kein Stop; nicht gesetzt=Fallback 'stop.bat' +# $startBat - (optional) Start-Skript-Dateiname; nicht gesetzt=Fallback 'dgl.bat' +# +# Strategie: Transaktionales Update mit vollständigem ZIP-Snapshot vor jeder +# Änderung. Bei Fehler wird der vorherige Stand automatisch 1:1 wiederhergestellt. $ErrorActionPreference = 'Stop' $DistBase = "https://updates.rhino.nrw/rhino/StatusQuo_Updates/raw/branch/main/$proj" -# Stop/Start-Namen VOR jeder Ueberschreibung aus dem aufrufenden Scope lesen $_stopName = if ($null -ne $stopBat) { $stopBat } else { 'stop.bat' } $_startName = if ($null -ne $startBat) { $startBat } else { 'dgl.bat' } -# Lokale Version lesen +# --------------------------------------------------------------------------- +# Hilfsfunktionen +# --------------------------------------------------------------------------- +function Stop-Portal { + if ($_stopName -ne '') { + $p = Join-Path $root $_stopName + if (Test-Path -LiteralPath $p) { & cmd /c "`"$p`""; Start-Sleep -Seconds 2 } + else { Write-Host " (Stop-Skript '$_stopName' nicht gefunden — übersprungen)" -ForegroundColor DarkYellow } + } else { Write-Host " (Kein Stop-Skript konfiguriert — übersprungen)" -ForegroundColor DarkYellow } +} + +function Start-Portal { + $p = Join-Path $root $_startName + if (Test-Path -LiteralPath $p) { + Start-Process -FilePath 'cmd.exe' -ArgumentList "/c `"$p`"" -WindowStyle Hidden + Write-Host "Server gestartet." -ForegroundColor Green + } else { + Write-Host "Start-Skript '$_startName' nicht gefunden — bitte Server manuell starten." -ForegroundColor Yellow + } +} + +# Vollständiger Snapshot der Portal-Wurzel als ein ZIP (alles außer .backup + TempUpdate). +# Erfasst ALLE Unterordner inkl. data/ → echtes 1:1, inkl. später gelöschter Dateien. +function New-Snapshot { + $ts = Get-Date -Format 'yyyyMMdd-HHmmss' + $bk = Join-Path $root '.backup' + if (-not (Test-Path -LiteralPath $bk)) { New-Item -ItemType Directory -Path $bk -Force | Out-Null } + $zip = Join-Path $bk "snapshot-$ts.zip" + $exclude = @('.backup','TempUpdate') + $items = Get-ChildItem -LiteralPath $root -Force | Where-Object { $exclude -notcontains $_.Name } + if (-not $items) { throw "Portal-Wurzel ist leer — nichts zu sichern." } + Compress-Archive -Path $items.FullName -DestinationPath $zip -CompressionLevel Optimal -Force + # Integrität prüfen (öffnen + Eintragszahl) + Add-Type -AssemblyName System.IO.Compression.FileSystem + $z = [System.IO.Compression.ZipFile]::OpenRead($zip) + $cnt = $z.Entries.Count + $z.Dispose() + if ($cnt -lt 1) { throw "Snapshot ist leer/ungültig." } + # Aufbewahrung: max. 5 Snapshots + Get-ChildItem -LiteralPath $bk -Filter 'snapshot-*.zip' | Sort-Object Name -Descending | + Select-Object -Skip 5 | Remove-Item -Force -ErrorAction SilentlyContinue + return $zip +} + +function Restore-Snapshot { + param([string]$Zip) + # Wurzel leeren (außer .backup), dann Snapshot zurückspielen + Get-ChildItem -LiteralPath $root -Force | Where-Object { $_.Name -ne '.backup' } | + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + Expand-Archive -LiteralPath $Zip -DestinationPath $root -Force +} + +# --------------------------------------------------------------------------- +# 1. Versionsvergleich +# --------------------------------------------------------------------------- $localVer = '0.0.0' $verFile = Join-Path $root 'VERSION' if (Test-Path -LiteralPath $verFile) { $localVer = (Get-Content $verFile -Raw).Trim() } -# Remote-Version abfragen Write-Host "Prüfe Update für $proj ..." try { $remoteVer = (Invoke-WebRequest -Uri "$DistBase/VERSION" -UseBasicParsing -TimeoutSec 15).Content.Trim() @@ -31,20 +84,26 @@ try { Write-Host "Lokal: v$localVer" Write-Host "Remote: v$remoteVer" -if ($localVer -eq $remoteVer) { - Write-Host "Bereits auf aktuellem Stand. Kein Update nötig." -ForegroundColor Green +# Nur echtes Upgrade anbieten (kein versehentlicher Downgrade) +$isNewer = $false +try { $isNewer = ([System.Version]$remoteVer -gt [System.Version]$localVer) } +catch { $isNewer = ($remoteVer -ne $localVer) } # nicht-numerische Version: Fallback +if (-not $isNewer) { + if ($localVer -eq $remoteVer) { Write-Host "Bereits auf aktuellem Stand. Kein Update nötig." -ForegroundColor Green } + else { Write-Host "Remote (v$remoteVer) ist nicht neuer als lokal (v$localVer) — kein Update." -ForegroundColor Yellow } return } $yn = Read-Host "Update von v$localVer auf v$remoteVer installieren? [j/N]" if ($yn -notin 'j','J','y','Y') { Write-Host "Abgebrochen."; return } -# Temp-Ordner +# --------------------------------------------------------------------------- +# 2. Bundles herunterladen (Server läuft noch) +# --------------------------------------------------------------------------- $tmp = Join-Path $root 'TempUpdate' Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $tmp | Out-Null -# Bundles laden Write-Host "Lade Manifest ..." $manifest = (Invoke-WebRequest -Uri "$DistBase/MANIFEST.txt" -UseBasicParsing -TimeoutSec 30).Content $bundles = ($manifest -split "`n" | Where-Object { $_ -match '^sync-bundle-' } | @@ -57,110 +116,92 @@ foreach ($b in $bundles) { Write-Host " sync-entpacken.bat ..." Invoke-WebRequest -Uri "$DistBase/sync-entpacken.bat" -OutFile (Join-Path $tmp 'sync-entpacken.bat') -UseBasicParsing -TimeoutSec 30 -# --- CHECKSUMMEN VERIFIZIEREN (optional — wird übersprungen wenn keine CHECKSUMS.txt vorhanden) --- -Write-Host "Verifiziere Checksummen ..." +# --------------------------------------------------------------------------- +# 3. Download-Integrität prüfen (SHA256, falls CHECKSUMS.txt vorhanden) +# Schützt gegen unvollständige/korrupte Downloads — NICHT gegen manipulierte +# Releases (gleicher Kanal). Authentizität = Release-Disziplin + Tests. +# --------------------------------------------------------------------------- +Write-Host "Verifiziere Download-Integrität ..." try { - $chkContent = (Invoke-WebRequest -Uri "$DistBase/CHECKSUMS.txt" -UseBasicParsing -TimeoutSec 15).Content + $chk = (Invoke-WebRequest -Uri "$DistBase/CHECKSUMS.txt" -UseBasicParsing -TimeoutSec 15).Content $allOk = $true - foreach ($line in ($chkContent -split "`n")) { + foreach ($line in ($chk -split "`n")) { $line = $line.Trim() if ($line -eq '' -or $line.StartsWith('#')) { continue } - # Format: SHA256HASH filename $parts = $line -split '\s+', 2 if ($parts.Count -lt 2) { continue } $expected = $parts[0].ToLower() - $filename = $parts[1].Trim() + $filename = $parts[1].Trim().TrimStart('*') # sha256sum nutzt evtl. '*' Prefix $localPath = Join-Path $tmp $filename if (-not (Test-Path -LiteralPath $localPath)) { continue } $actual = (Get-FileHash -LiteralPath $localPath -Algorithm SHA256).Hash.ToLower() if ($actual -ne $expected) { Write-Host "FEHLER: Checksumme stimmt nicht für $filename!" -ForegroundColor Red - Write-Host " Erwartet: $expected" -ForegroundColor Red - Write-Host " Erhalten: $actual" -ForegroundColor Red $allOk = $false } } if (-not $allOk) { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue - Write-Host "Update abgebrochen — keine Dateien verändert. Bitte Support kontaktieren." -ForegroundColor Red + Write-Host "Update abgebrochen — keine Datei verändert. Bitte Support kontaktieren." -ForegroundColor Red return } Write-Host " Alle Checksummen OK." -ForegroundColor Green } catch { - # CHECKSUMS.txt nicht vorhanden — Prüfung überspringen (abwärtskompatibel) - Write-Host " (Keine CHECKSUMS.txt — Prüfung übersprungen)" -ForegroundColor DarkGray + Write-Host " (Keine CHECKSUMS.txt — Integritätsprüfung übersprungen)" -ForegroundColor DarkGray } -# --- END CHECKSUMMEN --- -# Server stoppen +# --------------------------------------------------------------------------- +# 4. Server stoppen +# --------------------------------------------------------------------------- Write-Host "Stoppe Server ..." -if ($_stopName -ne '') { - $_stopPath = Join-Path $root $_stopName - if (Test-Path -LiteralPath $_stopPath) { - & cmd /c "`"$_stopPath`"" - Start-Sleep -Seconds 2 - } else { - Write-Host " (Stop-Skript '$_stopName' nicht gefunden — übersprungen)" -ForegroundColor DarkYellow +Stop-Portal + +# --------------------------------------------------------------------------- +# 5. Snapshot (transaktionssicher: ohne gültiges Backup kein Update) +# --------------------------------------------------------------------------- +Write-Host "Erstelle vollständige Sicherung ..." +try { + $snap = New-Snapshot + $snapSize = '{0:N1} MB' -f ((Get-Item -LiteralPath $snap).Length / 1MB) + Write-Host " Snapshot: $snap ($snapSize)" -ForegroundColor Cyan +} catch { + Write-Host "FEHLER beim Sichern: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Update abgebrochen — keine Datei verändert. Server wird wieder gestartet." -ForegroundColor Yellow + Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue + Start-Portal + return +} + +# --------------------------------------------------------------------------- +# 6. Update anwenden — bei Fehler automatischer Rollback +# --------------------------------------------------------------------------- +Write-Host "Wende Update an ..." +try { + & cmd /c "`"$(Join-Path $tmp 'sync-entpacken.bat')`"" + if ($LASTEXITCODE -ne 0) { throw "sync-entpacken.bat meldete Exitcode $LASTEXITCODE" } + [System.IO.File]::WriteAllText($verFile, ($remoteVer + "`r`n"), [System.Text.Encoding]::UTF8) +} catch { + Write-Host "FEHLER beim Anwenden: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Stelle vorherigen Stand automatisch wieder her ..." -ForegroundColor Yellow + try { + Restore-Snapshot -Zip $snap + Write-Host " Vorheriger Stand (v$localVer) wiederhergestellt." -ForegroundColor Green + } catch { + Write-Host " AUTO-ROLLBACK FEHLGESCHLAGEN! Bitte rollback.bat doppelklicken." -ForegroundColor Red } -} else { - Write-Host " (Kein Stop-Skript konfiguriert — übersprungen)" -ForegroundColor DarkYellow + Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue + Start-Portal + return } -# --- BACKUP (vor jeder Dateiänderung) --- -$ts = Get-Date -Format 'yyyyMMdd-HHmmss' -$bkDir = Join-Path $root ".backup\$ts" -New-Item -ItemType Directory -Path $bkDir -Force | Out-Null - -# 1. Nutzerdaten sichern (kritisch!) -$_dataPath = Join-Path $root 'data' -if (Test-Path -LiteralPath $_dataPath) { - Copy-Item -Path $_dataPath -Destination (Join-Path $bkDir 'data') -Recurse -Force - $dataCount = (Get-ChildItem -LiteralPath $_dataPath -Recurse -File -ErrorAction SilentlyContinue).Count - Write-Host " Nutzerdaten gesichert ($dataCount Dateien)" -ForegroundColor Cyan -} else { - Write-Host " (Kein data/-Ordner gefunden — übersprungen)" -ForegroundColor DarkGray -} - -# 2. Code-Dateien sichern -foreach ($ext in '*.ps1','*.html','*.js','*.css','*.bat','VERSION') { - Get-ChildItem -LiteralPath $root -File -Filter $ext -ErrorAction SilentlyContinue | - Copy-Item -Destination $bkDir -Force -} -Write-Host "Backup erstellt: $bkDir" -ForegroundColor Cyan - -# 3. Alte Backups bereinigen (max. 5 behalten) -$_backupBase = Join-Path $root '.backup' -$_allBackups = Get-ChildItem -LiteralPath $_backupBase -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -match '^\d{8}-\d{6}$' } | Sort-Object Name -Descending -if ($_allBackups.Count -gt 5) { - $_allBackups | Select-Object -Skip 5 | ForEach-Object { - Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue - } - Write-Host " (Nur 5 neueste Backups behalten)" -ForegroundColor DarkGray -} -# --- END BACKUP --- - -# Entpacken (sync-entpacken.bat aus TempUpdate → schreibt in Root eine Ebene hoeher) -Write-Host "Entpacke Update-Dateien ..." -& cmd /c "`"$(Join-Path $tmp 'sync-entpacken.bat')`"" - -# VERSION lokal aktualisieren -[System.IO.File]::WriteAllText($verFile, ($remoteVer + "`r`n"), [System.Text.Encoding]::UTF8) -Write-Host "Version auf v$remoteVer aktualisiert." -ForegroundColor Green - -# Aufräumen +# --------------------------------------------------------------------------- +# 7. Aufräumen + Start +# --------------------------------------------------------------------------- Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue - -# Server neu starten -$_startPath = Join-Path $root $_startName -if (Test-Path -LiteralPath $_startPath) { - Start-Process -FilePath 'cmd.exe' -ArgumentList "/c `"$_startPath`"" -WindowStyle Hidden - Write-Host "Server gestartet." -ForegroundColor Green -} else { - Write-Host "Start-Skript '$_startName' nicht gefunden — bitte Server manuell starten." -ForegroundColor Yellow -} +Write-Host "Version auf v$remoteVer aktualisiert." -ForegroundColor Green +Start-Portal Write-Host "" Write-Host "=== Update abgeschlossen ===" -ForegroundColor Green -Write-Host " Backup liegt unter: $bkDir" -ForegroundColor DarkGray -Write-Host " Bei Problemen: rollback.bat doppelklicken!" -ForegroundColor DarkGray +Write-Host " Sicherung: $snap" -ForegroundColor DarkGray +Write-Host " Bei Problemen: rollback.bat doppelklicken (funktioniert auch offline)." -ForegroundColor DarkGray