diff --git a/emmc-provisioning/dashboard/app.py b/emmc-provisioning/dashboard/app.py index 4110a05..21ef57e 100644 --- a/emmc-provisioning/dashboard/app.py +++ b/emmc-provisioning/dashboard/app.py @@ -18,7 +18,7 @@ import urllib.request from functools import wraps from pathlib import Path -from flask import Flask, render_template, jsonify, request, send_file, redirect, url_for, session +from flask import Flask, render_template, jsonify, request, send_file, redirect, url_for, session, Response, stream_with_context from werkzeug.security import generate_password_hash, check_password_hash @@ -871,9 +871,50 @@ def api_device_action_poll(): @app.route("/api/golden-image") def api_golden_image(): - """Stream the golden image for network deploy (device pulls and writes to eMMC).""" + """Stream the golden image for network deploy (device pulls and writes to eMMC). Decompresses .img.xz/.img.gz on the fly.""" if not GOLDEN_IMAGE.is_file(): return jsonify({"error": "Golden image not found"}), 404 + resolved = GOLDEN_IMAGE.resolve() + if str(resolved).endswith(".img.xz"): + def stream(): + proc = subprocess.Popen( + ["xz", "-d", "-c", str(resolved)], + stdout=subprocess.PIPE, + bufsize=65536, + ) + try: + while True: + chunk = proc.stdout.read(65536) + if not chunk: + break + yield chunk + finally: + proc.wait() + return Response( + stream_with_context(stream()), + mimetype="application/octet-stream", + headers={"Content-Disposition": "attachment; filename=golden.img"}, + ) + if str(resolved).endswith(".img.gz"): + def stream(): + proc = subprocess.Popen( + ["gzip", "-c", "-d", str(resolved)], + stdout=subprocess.PIPE, + bufsize=65536, + ) + try: + while True: + chunk = proc.stdout.read(65536) + if not chunk: + break + yield chunk + finally: + proc.wait() + return Response( + stream_with_context(stream()), + mimetype="application/octet-stream", + headers={"Content-Disposition": "attachment; filename=golden.img"}, + ) return send_file( GOLDEN_IMAGE, mimetype="application/octet-stream", @@ -1515,9 +1556,14 @@ def _build_status_write(phase, message, output_name=None, error=None): pass -def _raspios_latest_url(variant="lite"): - """Resolve latest Raspberry Pi OS (arm64) .img.xz URL. variant=lite|full.""" - slug = "raspios_lite_arm64" if variant == "lite" else "raspios_full_arm64" +def _raspios_latest_url(variant="desktop"): + """Resolve latest Raspberry Pi OS (arm64) .img.xz URL. variant=lite|desktop|full.""" + if variant == "lite": + slug = "raspios_lite_arm64" + elif variant == "full": + slug = "raspios_full_arm64" + else: + slug = "raspios_arm64" # desktop (with desktop, not full) base = f"https://downloads.raspberrypi.com/{slug}/images" headers = {"User-Agent": "Mozilla/5.0 (compatible; CM4-Provisioning/1.0)"} try: @@ -1578,9 +1624,9 @@ def api_build_cloudinit(): if st.get("phase") in ("downloading", "decompressing", "injecting", "finalizing", "resolving"): return jsonify({"ok": False, "error": "A build is already in progress"}), 409 body = request.get_json(silent=True) or {} - variant = (body.get("variant") or "lite").strip().lower() - if variant not in ("lite", "full"): - variant = "lite" + variant = (body.get("variant") or "desktop").strip().lower() + if variant not in ("lite", "desktop", "full"): + variant = "desktop" _build_status_write("resolving", "Resolving latest Raspberry Pi OS image URL…") url = _raspios_latest_url(variant) if not url: @@ -1610,10 +1656,10 @@ def api_build_cloudinit(): @app.route("/api/raspios-latest-url") @require_admin def api_raspios_latest_url(): - """Return the URL of the latest Raspberry Pi OS (arm64) image. Query: variant=lite|full.""" - variant = (request.args.get("variant") or "lite").strip().lower() - if variant not in ("lite", "full"): - variant = "lite" + """Return the URL of the latest Raspberry Pi OS (arm64) image. Query: variant=lite|desktop|full.""" + variant = (request.args.get("variant") or "desktop").strip().lower() + if variant not in ("lite", "desktop", "full"): + variant = "desktop" url = _raspios_latest_url(variant) if not url: return jsonify({"ok": False, "url": None, "error": "Could not resolve latest image URL"}), 503 diff --git a/emmc-provisioning/dashboard/templates/cloudinit_build.html b/emmc-provisioning/dashboard/templates/cloudinit_build.html index 767e48f..1e8b8db 100644 --- a/emmc-provisioning/dashboard/templates/cloudinit_build.html +++ b/emmc-provisioning/dashboard/templates/cloudinit_build.html @@ -67,7 +67,7 @@
Download latest Raspberry Pi OS (arm64), inject cloud-init. Output goes to Cloud-init images on the Admin page.