From d39052c1ea7710585e0e24a2fd1125c2eb54caf8 Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Do, 23 Jul 2026 14:53:39 +0200
Subject: [PATCH] Installer-Dateien direkt ins Einrichtungs-Paket buendeln

---
 app.py |   98 ++++++++++++++++++++++++++++++-------------------
 1 files changed, 60 insertions(+), 38 deletions(-)

diff --git a/app.py b/app.py
index dd6e9f4..a50b718 100644
--- a/app.py
+++ b/app.py
@@ -22,7 +22,7 @@
 # jederzeit im Fuss der Seitenleiste sichtbar ist, ob wirklich die neueste
 # Version laeuft (nicht nur auf der Festplatte liegt, sondern auch vom
 # gestarteten Programm geladen wurde).
-APP_VERSION = "2026-07-22 · r15 (Firewall-Freigabe-Skript ergänzt)"
+APP_VERSION = "2026-07-23 · r16 (Installer-Dateien direkt im Paket, kein Netzwerk zur Laufzeit nötig)"
 
 
 @app.context_processor
@@ -189,8 +189,7 @@
     "@echo off\r\n"
     "set ZIEL=C:\\ClientBibliothek\r\n"
     'if not exist "%ZIEL%" mkdir "%ZIEL%"\r\n'
-    'copy /Y "%~dp0einrichten.ps1" "%ZIEL%\\einrichten.ps1" >nul\r\n'
-    'copy /Y "%~f0" "%ZIEL%\\Rechner-einrichten-starten.bat" >nul\r\n'
+    'xcopy "%~dp0*" "%ZIEL%\\" /E /I /Y >nul\r\n'
     "echo ============================================================\r\n"
     "echo  Rechner-Einrichtung wird gestartet...\r\n"
     "echo  Admin-Rechte werden bei Bedarf per Windows-Abfrage angefragt.\r\n"
@@ -203,10 +202,12 @@
 
 
 def _erstelle_einrichtungs_skript(server_url, mitarbeiter, tools):
-    """Baut den Inhalt der einrichten.ps1 zusammen: laedt jedes ausgewaehlte
-    Tool ueber die bestehende Download-Route herunter, installiert es mit
-    dem hinterlegten Silent-Parameter und meldet das Ergebnis am Ende als
-    Protokoll an die Server-Bibliothek zurueck."""
+    """Baut den Inhalt der einrichten.ps1 zusammen: installiert jedes
+    ausgewaehlte Tool aus der MITGELIEFERTEN Datei (liegt im 'tools'-Unterordner
+    desselben Pakets, kein Netzwerkzugriff auf den Server noetig) und meldet
+    das Ergebnis am Ende BEST-EFFORT als Protokoll an die Server-Bibliothek
+    zurueck (falls Netzwerkzugriff vorhanden ist - schlaegt das fehl, bleibt
+    trotzdem das lokale Protokoll erhalten)."""
     zeilen = [
         '$ErrorActionPreference = "Stop"',
         f"$server = '{_ps_escape(server_url)}'",
@@ -218,10 +219,9 @@
         "    exit",
         "}",
         "",
-        "$tempOrdner = Join-Path $clientOrdner \"downloads\"",
+        "$toolsOrdner = Join-Path $clientOrdner \"tools\"",
         "$paketeOrdner = Join-Path $clientOrdner \"pakete\"",
         "$protokollDatei = Join-Path $clientOrdner \"protokoll.log\"",
-        "New-Item -ItemType Directory -Force -Path $tempOrdner | Out-Null",
         "New-Item -ItemType Directory -Force -Path $paketeOrdner | Out-Null",
         "",
         "try {",
@@ -246,41 +246,38 @@
         "",
         "function InstalliereTool($id, $name, $endung, $parameter) {",
         '    Write-Host "Installiere: $name ..." -ForegroundColor Cyan',
-        '    $zielDatei = Join-Path $tempOrdner ("sb_" + $id + $endung)',
-        "    try {",
-        '        Invoke-WebRequest -Uri "$server/software/$id/download" -OutFile $zielDatei -UseBasicParsing',
-        "    } catch {",
-        '        Write-Host ("  FEHLER beim Herunterladen von " + $name + ": " + $_) -ForegroundColor Red',
-        '        Protokolliere ($name + ": FEHLER beim Herunterladen")',
+        '    $quelle = Join-Path $toolsOrdner ($id.ToString() + $endung)',
+        "    if (-not (Test-Path $quelle)) {",
+        '        Write-Host ("  FEHLER: Datei fehlt im Paket unter " + $quelle) -ForegroundColor Red',
+        '        Protokolliere ($name + ": FEHLER, Datei fehlt im Paket")',
         "        return $false",
         "    }",
         "    try {",
         '        if ($endung -eq ".msi") {',
-        "            Start-Process \"msiexec.exe\" -ArgumentList ('/i \"' + $zielDatei + '\" ' + $parameter) -Wait",
+        "            Start-Process \"msiexec.exe\" -ArgumentList ('/i \"' + $quelle + '\" ' + $parameter) -Wait",
         "        } else {",
-        "            Start-Process $zielDatei -ArgumentList $parameter -Wait",
+        "            Start-Process $quelle -ArgumentList $parameter -Wait",
         "        }",
         "    } catch {",
         '        Write-Host ("  FEHLER bei der Installation von " + $name + ": " + $_) -ForegroundColor Red',
         '        Protokolliere ($name + ": FEHLER bei der Installation")',
         "        return $false",
         "    }",
-        "    Remove-Item -Path $zielDatei -Force -ErrorAction SilentlyContinue",
         '    Protokolliere ($name + ": installiert")',
         '    Write-Host "  fertig." -ForegroundColor Green',
         "    return $true",
         "}",
         "",
         "function BereitstellenPortablesTool($id, $name) {",
-        '    Write-Host "Lade herunter (portables Tool): $name ..." -ForegroundColor Cyan',
-        '    $zielZip = Join-Path $paketeOrdner ($name + ".zip")',
-        "    try {",
-        '        Invoke-WebRequest -Uri "$server/software/$id/download" -OutFile $zielZip -UseBasicParsing',
-        "    } catch {",
-        '        Write-Host ("  FEHLER beim Herunterladen von " + $name + ": " + $_) -ForegroundColor Red',
-        '        Protokolliere ($name + ": FEHLER beim Herunterladen")',
+        '    Write-Host "Portables Tool: $name ..." -ForegroundColor Cyan',
+        '    $quelle = Join-Path $toolsOrdner ($id.ToString() + ".zip")',
+        "    if (-not (Test-Path $quelle)) {",
+        '        Write-Host ("  FEHLER: ZIP fehlt im Paket unter " + $quelle) -ForegroundColor Red',
+        '        Protokolliere ($name + ": FEHLER, ZIP fehlt im Paket")',
         "        return $false",
         "    }",
+        '    $zielZip = Join-Path $paketeOrdner ($name + ".zip")',
+        "    Copy-Item -Path $quelle -Destination $zielZip -Force",
         '    Protokolliere ($name + ": als ZIP bereitgestellt unter " + $zielZip)',
         '    Write-Host ("  fertig, liegt als ZIP unter " + $zielZip) -ForegroundColor Green',
         '    Write-Host "  (bewusst NICHT automatisch entpackt - bitte manuell an den Zielort entpacken)" -ForegroundColor DarkGray',
@@ -304,7 +301,7 @@
     zeilen += [
         "",
         'Write-Host ""',
-        'Write-Host "Protokoll wird auf dem Server gespeichert..."',
+        'Write-Host "Protokoll wird an den Server gemeldet (optional, braucht Netzwerkzugriff)..."',
         '$koerper = "mitarbeiter=" + [uri]::EscapeDataString($mitarbeiter)',
         '$koerper += "&rechnername=" + [uri]::EscapeDataString($env:COMPUTERNAME)',
         '$koerper += "&installiert_von=" + [uri]::EscapeDataString("Automatisches Einrichtungs-Skript")',
@@ -312,9 +309,9 @@
         "foreach ($id in $erfolgreicheIds) { $koerper += \"&software_ids=$id\" }",
         "try {",
         '    Invoke-RestMethod -Uri "$server/zuweisungen/neu" -Method POST -Body $koerper -ContentType "application/x-www-form-urlencoded" | Out-Null',
-        '    Write-Host "Protokoll gespeichert." -ForegroundColor Green',
+        '    Write-Host "Zentrales Protokoll auf dem Server aktualisiert." -ForegroundColor Green',
         "} catch {",
-        '    Write-Host ("Konnte Protokoll nicht speichern: " + $_) -ForegroundColor Yellow',
+        '    Write-Host "Kein Netzwerkzugriff auf den Server - zentrales Protokoll nicht aktualisiert (lokales Protokoll unter protokoll.log ist trotzdem vollstaendig)." -ForegroundColor Yellow',
         "}",
         "",
         'Write-Host ""',
@@ -353,6 +350,7 @@
                 "typ": "installer",
                 "endung": os.path.splitext(gefunden)[1].lower(),
                 "parameter": sw["install_parameter"] or "",
+                "installer_pfad": pfad,
             })
             continue
 
@@ -367,6 +365,7 @@
                     "typ": "ordner",
                     "endung": "",
                     "parameter": "",
+                    "installer_pfad": pfad,
                 })
                 continue
 
@@ -388,6 +387,20 @@
         with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf:
             zf.writestr("einrichten.ps1", ps1_inhalt)
             zf.writestr("Rechner-einrichten-starten.bat", _BAT_LAUNCHER)
+
+            # Die echten Installer-Dateien/ZIPs direkt mit ins Paket packen,
+            # damit auf dem Zielrechner beim Ausfuehren KEIN Netzwerkzugriff
+            # auf den Server mehr noetig ist (nur die abschliessende
+            # Protokoll-Rueckmeldung ist optional und best-effort).
+            for t in ausgewaehlt:
+                if t["typ"] == "installer":
+                    quelldatei = updater.verwaltete_datei_pfad(t["installer_pfad"], t["name"])
+                    if quelldatei and os.path.isfile(quelldatei):
+                        zf.write(quelldatei, f"tools/{t['id']}{t['endung']}")
+                else:
+                    ordner = updater.effektiver_ablageordner(t["installer_pfad"], t["name"])
+                    zf.writestr(f"tools/{t['id']}.zip", _zippe_ordner_in_bytes(ordner))
+
         speicher.seek(0)
         return send_file(
             speicher, as_attachment=True,
@@ -397,6 +410,21 @@
     return render_template(
         "einrichten.html", installierbar=installierbar, nicht_installierbar=nicht_installierbar
     )
+
+
+def _zippe_ordner_in_bytes(ordner):
+    """Packt einen Ordner (ohne '_sicherungen'-Unterordner) in ein ZIP im
+    Arbeitsspeicher und gibt die rohen Bytes zurueck. Wird sowohl fuer den
+    Download-Button als auch fuer das Einrichtungs-Paket verwendet."""
+    speicher = io.BytesIO()
+    with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf:
+        for wurzel, unterordner, dateien in os.walk(ordner):
+            unterordner[:] = [o for o in unterordner if o != "_sicherungen"]
+            for datei in dateien:
+                voller_pfad = os.path.join(wurzel, datei)
+                rel_pfad = os.path.relpath(voller_pfad, ordner)
+                zf.write(voller_pfad, rel_pfad)
+    return speicher.getvalue()
 
 
 def _ordner_fuer_installer_sicherstellen(installer_pfad, name):
@@ -913,17 +941,11 @@
         flash(f"Ordner nicht gefunden unter: {ordner}", "fehler")
         return redirect(url_for("software_detail", software_id=software_id))
 
-    speicher = io.BytesIO()
-    with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf:
-        for wurzel, unterordner, dateien in os.walk(ordner):
-            unterordner[:] = [o for o in unterordner if o != "_sicherungen"]
-            for datei in dateien:
-                voller_pfad = os.path.join(wurzel, datei)
-                rel_pfad = os.path.relpath(voller_pfad, ordner)
-                zf.write(voller_pfad, rel_pfad)
-    speicher.seek(0)
+    zip_bytes = _zippe_ordner_in_bytes(ordner)
     zip_name = os.path.basename(ordner.rstrip("\\/")) + ".zip"
-    return send_file(speicher, as_attachment=True, download_name=zip_name, mimetype="application/zip")
+    return send_file(
+        io.BytesIO(zip_bytes), as_attachment=True, download_name=zip_name, mimetype="application/zip"
+    )
 
 
 if __name__ == "__main__":

--
Gitblit v1.9.3