Server-Bibliothek
2026-07-23 d39052c1ea7710585e0e24a2fd1125c2eb54caf8
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__":