feat: APK distribution pipeline — server hosting + Pi auto-update + /app routes

This commit is contained in:
Mathieu Moran
2026-08-15 20:52:38 -04:00
parent 5039b90bc6
commit 5a5df2b323
11 changed files with 320 additions and 16 deletions
+90
View File
@@ -0,0 +1,90 @@
#!/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())