feat: APK distribution pipeline — server hosting + Pi auto-update + /app routes
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user