#!/usr/bin/env python3 """Mirror the latest Android APK from the remote server to the Pi's hotspot. Polls GET /api/v1/app/latest on the server, downloads /app/yaris-obd.apk when the sha256 differs from the local copy, verifies it, and atomically replaces static/app/yaris-obd.apk — so the phone can install from http://10.42.0.1/app without leaving the car WiFi. Runs on a systemd timer (see deploy/yaris-app-updater.{service,timer}). Stdlib only — no pip dependencies. """ import hashlib import json import os import sys import urllib.error import urllib.request SERVER_URL = os.environ.get("YARIS_SERVER_URL", "https://yaris.ceaf.dev").rstrip("/") APP_DIR = os.environ.get("YARIS_APP_DIR", "/home/yaris/app/yaris-obd/static/app") APK_PATH = os.path.join(APP_DIR, "yaris-obd.apk") def sha256_file(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def fetch_json(url, timeout=20): with urllib.request.urlopen(url, timeout=timeout) as r: return json.load(r) def main(): try: manifest = fetch_json(f"{SERVER_URL}/api/v1/app/latest") except urllib.error.HTTPError as e: if e.code == 404: print("No app published on the server yet; skipping.") return 0 print(f"latest check failed: HTTP {e.code}") return 1 except Exception as e: print(f"latest check failed: {e}") return 1 sha = manifest.get("sha256") if not sha: print("manifest missing sha256") return 1 if os.path.isfile(APK_PATH): try: if sha256_file(APK_PATH) == sha: print(f"APK up to date (version {manifest.get('version')})") return 0 except Exception as e: print(f"local hash failed: {e}") os.makedirs(APP_DIR, exist_ok=True) tmp = APK_PATH + ".part" try: with urllib.request.urlopen(f"{SERVER_URL}/app/yaris-obd.apk", timeout=600) as r, open(tmp, "wb") as f: while True: chunk = r.read(1024 * 1024) if not chunk: break f.write(chunk) if sha256_file(tmp) != sha: print("downloaded APK sha256 mismatch — discarding") os.remove(tmp) return 1 os.replace(tmp, APK_PATH) print(f"Updated APK to version {manifest.get('version')} ({os.path.getsize(APK_PATH)} bytes)") return 0 except Exception as e: print(f"download failed: {e}") if os.path.isfile(tmp): try: os.remove(tmp) except Exception: pass return 1 if __name__ == "__main__": sys.exit(main())