Initial commit: macOS USB Stick Creator - Docker Compose Web-GUI fuer bootfaehige macOS-Installer
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from flask import Flask, Response, jsonify, request, send_from_directory
|
||||
|
||||
import catalog
|
||||
import installer
|
||||
from devices import list_usb_sticks
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("app")
|
||||
|
||||
FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "..", "frontend")
|
||||
app = Flask(__name__, static_folder=None)
|
||||
|
||||
_catalog_cache = {"items": [], "catalog_url": None, "loaded_at": None, "error": None, "loading": True}
|
||||
_catalog_lock = threading.Lock()
|
||||
|
||||
|
||||
def _load_catalog():
|
||||
with _catalog_lock:
|
||||
_catalog_cache["loading"] = True
|
||||
try:
|
||||
items, url = catalog.list_macos_installers()
|
||||
with _catalog_lock:
|
||||
_catalog_cache.update(
|
||||
items=items, catalog_url=url, loaded_at=time.time(), error=None, loading=False
|
||||
)
|
||||
log.info("Katalog geladen: %d macOS-Installer gefunden (%s)", len(items), url)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.exception("Katalog-Laden fehlgeschlagen")
|
||||
with _catalog_lock:
|
||||
_catalog_cache.update(error=str(exc), loading=False)
|
||||
|
||||
|
||||
# Beim Container-Start einmal im Hintergrund laden, damit die GUI sofort
|
||||
# ansprechbar ist und nicht auf den ersten Request wartet.
|
||||
threading.Thread(target=_load_catalog, daemon=True).start()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
return send_from_directory(FRONTEND_DIR, "index.html")
|
||||
|
||||
|
||||
@app.get("/<path:filename>")
|
||||
def static_files(filename):
|
||||
return send_from_directory(FRONTEND_DIR, filename)
|
||||
|
||||
|
||||
@app.get("/api/versions")
|
||||
def api_versions():
|
||||
with _catalog_lock:
|
||||
return jsonify(dict(_catalog_cache))
|
||||
|
||||
|
||||
@app.post("/api/versions/refresh")
|
||||
def api_versions_refresh():
|
||||
threading.Thread(target=_load_catalog, daemon=True).start()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.get("/api/devices")
|
||||
def api_devices():
|
||||
try:
|
||||
return jsonify({"items": list_usb_sticks()})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.exception("device listing failed")
|
||||
return jsonify({"items": [], "error": str(exc)}), 500
|
||||
|
||||
|
||||
@app.post("/api/jobs")
|
||||
def api_create_job():
|
||||
body = request.get_json(force=True)
|
||||
product_id = body.get("product_id")
|
||||
device_path = body.get("device_path")
|
||||
confirm_path = body.get("confirm_device_path")
|
||||
|
||||
with _catalog_lock:
|
||||
product = next((p for p in _catalog_cache["items"] if p["id"] == product_id), None)
|
||||
if not product:
|
||||
return jsonify({"error": "Unbekannte macOS-Version -- Katalog neu laden."}), 400
|
||||
|
||||
try:
|
||||
job = installer.create_job(product, device_path, confirm_path)
|
||||
except ValueError as exc:
|
||||
return jsonify({"error": str(exc)}), 400
|
||||
|
||||
return jsonify({"job_id": job.id})
|
||||
|
||||
|
||||
@app.get("/api/jobs/<job_id>")
|
||||
def api_job_status(job_id):
|
||||
job = installer.JOBS.get(job_id)
|
||||
if not job:
|
||||
return jsonify({"error": "unbekannter Job"}), 404
|
||||
return jsonify(job.snapshot())
|
||||
|
||||
|
||||
@app.get("/api/jobs/<job_id>/stream")
|
||||
def api_job_stream(job_id):
|
||||
job = installer.JOBS.get(job_id)
|
||||
if not job:
|
||||
return jsonify({"error": "unbekannter Job"}), 404
|
||||
|
||||
def gen():
|
||||
for event in job.stream():
|
||||
yield f"data: {json.dumps(event)}\n\n"
|
||||
|
||||
return Response(gen(), mimetype="text/event-stream")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8080, threaded=True)
|
||||
Reference in New Issue
Block a user