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 +++++++++++++++---------
ARCHITEKTUR.md | 43 ++++++++--
CHANGELOG.md | 20 +++++
templates/einrichten.html | 24 +++--
README.md | 29 +++++++
5 files changed, 154 insertions(+), 60 deletions(-)
diff --git a/ARCHITEKTUR.md b/ARCHITEKTUR.md
index 3d69a05..e90bc6b 100644
--- a/ARCHITEKTUR.md
+++ b/ARCHITEKTUR.md
@@ -146,32 +146,53 @@
## 6. "Rechner einrichten" – Verteilungs-Mechanismus
Route `/einrichten` (app.py) erzeugt bei Bedarf ein individuelles
-Installations-Paket (ZIP mit `einrichten.ps1` + `Rechner-einrichten-starten.bat`),
+Installations-Paket (ZIP mit `einrichten.ps1` + `Rechner-einrichten-starten.bat`
+**+ den echten Installer-Dateien/ZIPs selbst** unter `tools/<id>.<endung>`),
statt vorab für jede Mitarbeiter/Tool-Kombination ein festes Skript zu pflegen.
+**Wichtige Design-Entscheidung (Runde r16):** Die Installer-Dateien werden
+bereits beim Erzeugen des Pakets (auf dem PC, der die Checkliste ausfüllt)
+eingebettet, nicht erst beim Ausführen auf dem Zielrechner nachgeladen. Ein
+früherer Ansatz lud die Dateien per `Invoke-WebRequest` zur Laufzeit vom
+Server nach – das erforderte Netzwerkzugriff des Zielrechners auf den Server
+in genau dem Moment der Einrichtung, was in der Praxis nicht immer gegeben
+ist (z. B. Rechner noch nicht ans Firmennetz angebunden). Jetzt braucht der
+Zielrechner für die eigentliche Installation **kein Netzwerk mehr** – nur die
+abschließende, optionale Meldung ans zentrale Protokoll (`/zuweisungen/neu`)
+versucht noch einen Netzwerkzugriff, scheitert dieser, bleibt trotzdem das
+lokale Protokoll (`protokoll.log`) vollständig.
+
Ablauf beim Ausführen auf dem Zielrechner:
-1. `.bat` kopiert sich selbst + das `.ps1` nach `C:\ClientBibliothek`
+1. `.bat` kopiert den **kompletten entpackten Ordner** (inkl. `tools/`-
+ Unterordner mit den echten Installer-Dateien) nach `C:\ClientBibliothek`
+ (per `xcopy /E /I /Y`)
2. Self-Elevation (Admin-Rechte-Anfrage) über `Start-Process -Verb RunAs`
3. Automatische Windows-Defender-Ausnahme für `C:\ClientBibliothek`
(`Add-MpPreference -ExclusionPath`) – ein einziger Pfad statt vieler
Einzelausnahmen
-4. Pro ausgewähltem Tool: entweder stille Installation (Download + Ausführung
- mit hinterlegtem Silent-Parameter, `.msi` läuft über `msiexec`) oder – bei
- Tools ohne einzelne Installer-Datei ("Ordner-Typ", z. B. portable
- Programme wie Blender) – Download als ZIP nach `C:\ClientBibliothek\pakete\`,
- **bewusst ohne automatisches Entpacken** (Entscheidung in Runde r14: der
- Zielort für portable Tools variiert, das soll eine Person bewusst
+4. Pro ausgewähltem Tool: entweder stille Installation aus der **lokal
+ mitgelieferten** Datei unter `tools/<id>.<endung>` (mit hinterlegtem
+ Silent-Parameter, `.msi` läuft über `msiexec`) oder – bei Tools ohne
+ einzelne Installer-Datei ("Ordner-Typ", z. B. portable Programme wie
+ Blender) – die mitgelieferte ZIP-Datei wird nach `C:\ClientBibliothek\pakete\`
+ kopiert, **bewusst ohne automatisches Entpacken** (Entscheidung in Runde
+ r14: der Zielort für portable Tools variiert, das soll eine Person bewusst
entscheiden)
5. Lokales Protokoll unter `C:\ClientBibliothek\protokoll.log`
-6. Automatischer POST an `/zuweisungen/neu` auf dem Server, um das zentrale
+6. Optionaler POST an `/zuweisungen/neu` auf dem Server, um das zentrale
Protokoll zu befüllen (Rechnername wird dabei über `$env:COMPUTERNAME`
- automatisch erkannt)
+ automatisch erkannt) – best effort, kein Abbruch bei Fehlschlag
Die Ordner-Struktur unter `C:\ClientBibliothek`:
-- `\downloads` – transiente Installer-Dateien, werden nach Gebrauch gelöscht
+- `\tools` – die mitgelieferten Installer-Dateien/ZIPs (Quelle für die
+ Installation, kommen direkt aus dem heruntergeladenen Paket)
- `\pakete` – ZIPs von portablen Tools, bleiben bewusst liegen
- `\protokoll.log` – lokale Mitschrift
+Serverseitig teilen sich `/einrichten` (POST) und `/software/<id>/download`
+die Hilfsfunktion `_zippe_ordner_in_bytes()` für das ZIP-Packen eines
+Ordners, um Code-Duplikation zu vermeiden.
+
**Der Client-Ordner-Pfad ist eine Konstante** (`CLIENT_ORDNER` in `app.py`,
aktuell `C:\ClientBibliothek`) – zentral anpassbar, falls die Infrastruktur
einen anderen Standardpfad vorgibt.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e0b841..edf3a0c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -93,6 +93,26 @@
automatisch selbst eingetragen (`Add-MpPreference`)
- Firewall-Freigabe-Skript ergänzt, nachdem der Test im echten
Firmennetzwerk anstand (Port 5000 war durch die Windows-Firewall blockiert)
+- **Installer-Dateien werden direkt ins Paket gebündelt statt zur Laufzeit
+ nachzuladen.** Auslöser: beim ersten Test im Firmennetz enthielt der
+ Download nur die Skript-Dateien, die eigentlichen Installer wurden erst
+ beim Ausführen auf dem Zielrechner per Netzwerk nachgeladen – das setzt
+ Netzwerkzugriff des Zielrechners auf den Server in genau dem Moment
+ voraus. Jetzt werden die Dateien beim Erzeugen des Pakets direkt mit
+ eingepackt, der Zielrechner braucht für die Installation selbst kein
+ Netzwerk mehr (nur die abschließende, optionale Protokoll-Meldung).
+
+## Offene Design-Frage: Skript-Ansatz vs. automatische Verteilung
+Nach dem ersten Test im Firmennetz wurde nochmal grundsätzlich hinterfragt,
+ob der Skript-Ansatz (manueller Download + Ausführen auf dem Zielrechner)
+der richtige Weg ist, oder ob Daten "automatisch" auf dem Zielrechner landen
+sollten. Eine echte automatische Verteilung ohne manuellen Schritt am
+Zielrechner würde entweder eine dauerhaft laufende Helfer-Software auf jedem
+Zielrechner oder Windows-Remoting (PowerShell Remoting) voraussetzen – beide
+Wege brauchen im Kern eine Domänen-Infrastruktur bzw. zentrale
+Zugangsdaten-Verwaltung, die aktuell nicht vorhanden ist. Diese Frage wurde
+bewusst nicht in diesem Prototyp entschieden, sondern als offener Punkt für
+die Übernahme festgehalten (siehe README.md).
## Testmethodik (durchgehend)
Da die Zielumgebung Windows ist, die Entwicklung aber in einer Linux-Umgebung
diff --git a/README.md b/README.md
index 7d74d6d..5d6ffd4 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,35 @@
---
+## 3a. Rechner einrichten (Verteilung an neue Rechner)
+
+Menüpunkt "Rechner einrichten": Häkchen bei den benötigten Tools setzen,
+Mitarbeiter eintragen, "Installations-Paket herunterladen" klicken. Das
+erzeugte ZIP enthält die ausgewählten Installer-Dateien **direkt mit** (kein
+Nachladen vom Server nötig).
+
+Auf dem **neuen Rechner**: ZIP entpacken, `Rechner-einrichten-starten.bat`
+doppelklicken. Das Skript:
+- fragt bei Bedarf automatisch nach Admin-Rechten
+- installiert jedes Tool still mit dem hinterlegten Silent-Parameter
+- trägt automatisch eine Windows-Defender-Ausnahme für `C:\ClientBibliothek`
+ ein (dort landen alle Dateien gesammelt)
+- meldet das Ergebnis am Ende automatisch ans zentrale Protokoll zurück
+ (Rechnername wird dabei selbst erkannt)
+
+Tools ohne einzelne Installer-Datei (z. B. bereits entpackte/portable Tools
+wie Blender) werden als ZIP nach `C:\ClientBibliothek\pakete\` gelegt –
+bewusst ohne automatisches Entpacken, das entscheidet ihr von Fall zu Fall
+selbst.
+
+⚠� Die Windows-spezifischen Teile (Admin-Rechte-Anfrage, stille Installation)
+wurden nicht auf einem echten Windows-Rechner entwickelt und getestet, nur
+mit dem echten PowerShell-Parser auf Syntaxfehler geprüft sowie die
+Netzwerk-/Datei-Logik separat verifiziert. Bei größerem Rollout zuerst an
+1–2 unkritischen Rechnern gegentesten.
+
+---
+
## 4. Automatische Aktualisierung
Bei jedem Tool kann beim Anlegen/Bearbeiten "Automatische Aktualisierung
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__":
diff --git a/templates/einrichten.html b/templates/einrichten.html
index 7086d46..52e392c 100644
--- a/templates/einrichten.html
+++ b/templates/einrichten.html
@@ -12,17 +12,19 @@
<div class="panel" style="margin-bottom:18px;">
<p style="margin:0 0 8px; font-size:13.5px;">
Häkchen bei den benötigten Tools setzen, Mitarbeiter eintragen und
- herunterladen. Auf dem <strong>neuen Rechner</strong> das ZIP entpacken
- und <code>Rechner-einrichten-starten.bat</code> doppelklicken. Alles landet
- dabei gesammelt unter <code>C:\ClientBibliothek</code> – dafür wird
- automatisch eine Windows-Defender-Ausnahme für genau diesen einen Ordner
- eingetragen. Tools mit Installer-Datei werden still installiert
- (<code>\downloads</code>, danach automatisch aufgeräumt). Tools ohne
- Installer ("als ZIP bereitgestellt", z. B. portable Tools) landen
- unangetastet als ZIP in <code>\pakete</code> – die entpackt ihr bewusst
- von Hand an den gewünschten Zielort. Anschließend trägt das Skript selbst
- ein Protokoll ein (zentral auf dem Server und lokal unter
- <code>\protokoll.log</code>).
+ herunterladen. Die ausgewählten Installer-Dateien werden dabei direkt mit
+ ins Paket gepackt – auf dem <strong>neuen Rechner</strong> braucht ihr nur
+ das ZIP zu entpacken und <code>Rechner-einrichten-starten.bat</code>
+ doppelzuklicken, ganz ohne dass der Zielrechner Netzwerkzugriff auf
+ diesen Server haben muss. Alles landet dabei gesammelt unter
+ <code>C:\ClientBibliothek</code> – dafür wird automatisch eine
+ Windows-Defender-Ausnahme für genau diesen einen Ordner eingetragen.
+ Tools mit Installer-Datei werden still installiert. Tools ohne Installer
+ ("als ZIP bereitgestellt", z. B. portable Tools) landen unangetastet als
+ ZIP in <code>\pakete</code> – die entpackt ihr bewusst von Hand an den
+ gewünschten Zielort. Nur die abschließende Meldung ans zentrale Protokoll
+ braucht (optional) Netzwerkzugriff – schlägt das fehl, bleibt trotzdem
+ das lokale Protokoll unter <code>\protokoll.log</code> vollständig.
</p>
</div>
--
Gitblit v1.9.3