feat: APK distribution pipeline — server hosting + Pi auto-update + /app routes
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time install of the Android APK auto-updater on the Pi.
|
||||
# Run ON THE PI as the `yaris` user (needs sudo):
|
||||
# cd ~/app/yaris-obd && bash deploy/install_app_updater.sh
|
||||
#
|
||||
# Installs a systemd timer that polls the remote server for the latest APK
|
||||
# and mirrors it to static/app/ so the phone can install from
|
||||
# http://10.42.0.1/app without leaving the car hotspot.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
sudo systemctl stop yaris-app-updater.timer yaris-app-updater.service 2>/dev/null || true
|
||||
sudo install -m 755 "$REPO_DIR/deploy/yaris_app_updater.py" /usr/local/bin/yaris-app-updater
|
||||
sudo install -m 644 "$REPO_DIR/deploy/yaris-app-updater.service" /etc/systemd/system/yaris-app-updater.service
|
||||
sudo install -m 644 "$REPO_DIR/deploy/yaris-app-updater.timer" /etc/systemd/system/yaris-app-updater.timer
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now yaris-app-updater.timer
|
||||
sudo systemctl start yaris-app-updater.service
|
||||
echo "APK updater installed. Check: systemctl status yaris-app-updater.service"
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=yaris-obd Android APK mirror from the remote server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
TimeoutStartSec=600
|
||||
Environment=YARIS_SERVER_URL=https://yaris.ceaf.dev
|
||||
Environment=YARIS_APP_DIR=/home/yaris/app/yaris-obd/static/app
|
||||
ExecStart=/usr/local/bin/yaris-app-updater
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Periodically mirror the Android APK to the car hotspot
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=1h
|
||||
RandomizedDelaySec=5min
|
||||
Unit=yaris-app-updater.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -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