feat(update): App laedt Updates per HTTP mit Fortschrittsanzeige statt als 65-MB-WebSocket-Block

Bisher kam die APK als EINE WebSocket-Nachricht (~65 MB Base64) mit starrem
60-s-Timeout — ueber den externen Proxy (Heim-Upload → Hetzner → Handy) zu
knapp, Abbruch ohne Fortschritt.

RVS: GET/HEAD /update/latest.apk — nur mit gueltigem Raum-Token im Header
X-RVS-Token (sonst 403), Content-Length, Range/206 zum Fortsetzen. Version
im Dateinamen wird ohne angehaengten Punkt erkannt.

App: Download per HTTP (react-native-fs) mit Fortschritt in % und MB,
Abbrechen-Knopf, Abbruch nur bei 30 s ohne Daten statt fester Gesamtzeit,
3 Versuche, Groessen-Check gegen Content-Length. Alter RVS (404): Fallback
auf den WebSocket-Weg mit 5 min Timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 13:10:07 +01:00
co-authored by Claude Opus 5.5
parent baf9e93c2a
commit a00199c121
5 changed files with 247 additions and 36 deletions
+46 -1
View File
@@ -403,6 +403,51 @@ function handleHttpRequest(req, res) {
return;
}
// App-Update als normaler HTTP-Download (statt eines riesigen WS-Blocks):
// Content-Length fuer die Fortschrittsanzeige, Range zum Fortsetzen.
// Nur fuer Geraete mit gueltigem Raum-Token (Header X-RVS-Token).
if ((req.method === "GET" || req.method === "HEAD") && pathname === "/update/latest.apk") {
const tok = String(req.headers["x-rvs-token"] || "");
if (!tok || !rooms.has(tok)) {
res.writeHead(403, { "Content-Type": "text/plain" });
res.end("Forbidden\n");
return;
}
const apk = getLatestAPK();
if (!apk) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Keine APK verfuegbar\n");
return;
}
const size = fs.statSync(apk.path).size;
const headers = {
"Content-Type": "application/vnd.android.package-archive",
"Content-Disposition": `attachment; filename="ARIA-v${apk.version}.apk"`,
"Accept-Ranges": "bytes",
"Cache-Control": "no-store",
"X-APK-Version": apk.version,
};
let start = 0, end = size - 1, status = 200;
const m = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range || ""));
if (m && (m[1] || m[2])) {
if (m[1]) { start = parseInt(m[1], 10); if (m[2]) end = Math.min(parseInt(m[2], 10), size - 1); }
else { start = Math.max(0, size - parseInt(m[2], 10)); }
if (start > end || start >= size) {
res.writeHead(416, { "Content-Range": `bytes */${size}` });
res.end();
return;
}
status = 206;
headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
}
headers["Content-Length"] = end - start + 1;
res.writeHead(status, headers);
if (req.method === "HEAD") { res.end(); return; }
log(`APK-Download (HTTP) v${apk.version}${status === 206 ? ` ab Byte ${start}` : ""}`);
fs.createReadStream(apk.path, { start, end }).pipe(res);
return;
}
// Health-Endpoint
if (req.method === "GET" && pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
@@ -743,7 +788,7 @@ function getLatestAPK() {
.filter(f => f.endsWith(".apk"))
.map(f => {
// ARIA-v0.0.2.3.apk oder ARIA-Cockpit-release.apk
const match = f.match(/(\d+\.\d+\.\d+[\.\d]*)/);
const match = f.match(/(\d+(?:\.\d+){2,})/);
return { file: f, path: path.join(UPDATES_DIR, f), version: match ? match[1] : null };
})
.filter(f => f.version)