diff --git a/.env.example b/.env.example index cd05863..f6fcf4b 100644 --- a/.env.example +++ b/.env.example @@ -18,7 +18,10 @@ TICKETMASTER_API_KEY= # ── Android APK download button + in-app updater ───────────────────────────── # The backend proxies the fixed Gitea release ("apk-latest") over the LAN, so Gitea can stay internal. -GITEA_URL=http://192.168.2.51:3000 # internal Gitea base reachable from the backend container +GITEA_URL=http://192.168.2.51:3000 # Gitea-Basis, die der BACKEND-CONTAINER erreicht. + # Muss nicht Giteas eigene ROOT_URL sein — das Backend baut + # die Asset-URL auf diesen Host um (Test aus dem Container: + # curl -sI $GITEA_URL/api/v1/version). GITEA_REPO=Docker/NovaSonic GITEA_TOKEN= # read token (only needed for a private repo) GITEA_APK_TAG=apk-latest diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 307109d..1c65add 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -503,6 +503,19 @@ function _giteaApiJson(pathAndQuery) { }); } +// Gitea baut seine browser_download_url aus der eigenen ROOT_URL — die kann eine ganz andere Adresse +// sein als die, unter der das Backend Gitea erreicht (interne IP vs. Reverse-Proxy-Domain). Deshalb +// Schema+Host+Port immer durch GITEA_URL ersetzen und nur den Pfad übernehmen. +function _viaGiteaHost(urlStr) { + if (!urlStr || !GITEA_URL) return urlStr; + try { + const base = new URL(GITEA_URL); + const u = new URL(urlStr); + if (u.origin === base.origin) return urlStr; + return new URL(u.pathname + u.search, base).toString(); + } catch { return urlStr; } +} + // The .apk asset of the fixed release, or null if unavailable. async function _giteaApkAsset() { const rel = await _giteaApiJson(`/releases/tags/${encodeURIComponent(GITEA_APK_TAG)}`); @@ -513,7 +526,7 @@ async function _giteaApkAsset() { const vcMatch = /versionCode\s*[=:]\s*(\d+)/i.exec(rel.body || ''); return { size: asset.size, - url: asset.browser_download_url, + url: _viaGiteaHost(asset.browser_download_url), version: rel.name || rel.tag_name || null, versionCode: vcMatch ? parseInt(vcMatch[1], 10) : null, updatedAt: new Date(asset.created_at || rel.published_at || Date.now()).getTime(), @@ -523,7 +536,13 @@ async function _giteaApkAsset() { function _serveLocalApk(res) { if (res.headersSent) return; fs.stat(APK_PATH, (err) => { - if (err) return res.status(404).json({ error: 'Keine APK hinterlegt' }); + // Ohne lokale Datei UND ohne erreichbares Gitea-Asset: sagen, welcher der beiden Wege fehlt, + // sonst sucht man die APK an der falschen Stelle (die Logs oben nennen den Grund). + if (err) return res.status(404).json({ + error: (GITEA_URL && GITEA_REPO) + ? `APK-Release nicht erreichbar (${GITEA_URL}, Tag ${GITEA_APK_TAG}) und keine lokale Datei unter ${APK_PATH}` + : 'Keine APK hinterlegt', + }); res.setHeader('Content-Type', 'application/vnd.android.package-archive'); res.setHeader('Content-Disposition', 'attachment; filename="NovaSonic.apk"'); res.sendFile(APK_PATH); @@ -539,14 +558,21 @@ function _streamApkFromUrl(urlStr, res, redirectsLeft = 3) { lib.get(u, { headers }, up => { if ([301, 302, 303, 307, 308].includes(up.statusCode) && up.headers.location && redirectsLeft > 0) { up.resume(); - return _streamApkFromUrl(new URL(up.headers.location, u).toString(), res, redirectsLeft - 1); + return _streamApkFromUrl(_viaGiteaHost(new URL(up.headers.location, u).toString()), res, redirectsLeft - 1); + } + if (up.statusCode !== 200) { + up.resume(); + console.warn(`[apk] Gitea-Asset lieferte HTTP ${up.statusCode} (${u.origin}) — fallback auf ${APK_PATH}`); + return _serveLocalApk(res); } - if (up.statusCode !== 200) { up.resume(); return _serveLocalApk(res); } res.setHeader('Content-Type', 'application/vnd.android.package-archive'); res.setHeader('Content-Disposition', 'attachment; filename="NovaSonic.apk"'); if (up.headers['content-length']) res.setHeader('Content-Length', up.headers['content-length']); up.pipe(res); - }).on('error', () => _serveLocalApk(res)); + }).on('error', (e) => { + console.warn(`[apk] Gitea-Asset nicht erreichbar (${u.origin}): ${e.message} — fallback auf ${APK_PATH}`); + _serveLocalApk(res); + }); } app.get('/api/apk/info', async (req, res) => {