const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { app } = require('electron'); const UPDATE_REPO = 'Administrator/Multi-Hoster-Upload'; const GITEA_BASE = 'https://git.24-music.de'; const API_URL = `${GITEA_BASE}/api/v1/repos/${UPDATE_REPO}/releases?limit=1`; const CHECK_TIMEOUT = 15000; let cachedCheck = null; let cachedCheckTs = 0; const CACHE_TTL = 10 * 60 * 1000; // 10 min let activeAbort = null; function getCurrentVersion() { return app.getVersion(); } function parseVersion(str) { const clean = String(str || '').replace(/^v/i, '').trim(); const parts = clean.split('.').map(Number); return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 }; } function isNewer(remote, current) { const r = parseVersion(remote); const c = parseVersion(current); if (r.major !== c.major) return r.major > c.major; if (r.minor !== c.minor) return r.minor > c.minor; return r.patch > c.patch; } function pickSetupAsset(assets) { if (!Array.isArray(assets)) return null; // Prefer asset with "setup" in the name (case-insensitive) const setup = assets.find(a => /setup/i.test(a.name) && /\.exe$/i.test(a.name) ); if (setup) return setup; // Fallback: any .exe return assets.find(a => /\.exe$/i.test(a.name)) || null; } function findLatestYml(assets) { if (!Array.isArray(assets)) return null; return assets.find(a => /^latest\.yml$/i.test(a.name)) || null; } async function fetchJson(url, signal) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT); if (signal) signal.addEventListener('abort', () => controller.abort()); try { const res = await fetch(url, { method: 'GET', signal: controller.signal, redirect: 'follow' }); return await res.json(); } finally { clearTimeout(timeout); } } async function checkForUpdate() { // Return cached result if fresh if (cachedCheck && (Date.now() - cachedCheckTs) < CACHE_TTL) { return cachedCheck; } const releases = await fetchJson(API_URL); if (!Array.isArray(releases) || releases.length === 0) { return { available: false }; } const release = releases[0]; const remoteVersion = release.tag_name || release.name || ''; const currentVersion = getCurrentVersion(); if (!isNewer(remoteVersion, currentVersion)) { cachedCheck = { available: false, currentVersion, remoteVersion }; cachedCheckTs = Date.now(); return cachedCheck; } const setupAsset = pickSetupAsset(release.assets); const latestYml = findLatestYml(release.assets); if (!setupAsset) { return { available: false, reason: 'Kein Setup-Asset im Release gefunden' }; } cachedCheck = { available: true, currentVersion, remoteVersion: remoteVersion.replace(/^v/i, ''), releaseUrl: release.html_url, assetUrl: setupAsset.browser_download_url, assetSize: setupAsset.size, assetName: setupAsset.name, latestYmlUrl: latestYml ? latestYml.browser_download_url : null, releaseNotes: release.body || '' }; cachedCheckTs = Date.now(); return cachedCheck; } async function parseLatestYml(url) { if (!url) return null; try { const res = await fetch(url, { redirect: 'follow' }); const text = await res.text(); // Extract sha512 from latest.yml const match = text.match(/sha512:\s*([A-Za-z0-9+/=]+)/); return match ? match[1] : null; } catch { return null; } } function verifyExeHeader(buf) { // Check MZ header if (buf.length < 128 * 1024) return false; return buf[0] === 0x4D && buf[1] === 0x5A; // 'MZ' } async function installUpdate(onProgress) { if (activeAbort) activeAbort.abort(); activeAbort = new AbortController(); const signal = activeAbort.signal; try { // Stage: starting if (onProgress) onProgress({ stage: 'starting', percent: 0 }); // Check or use cached let check = cachedCheck; if (!check || !check.available) { check = await checkForUpdate(); } if (!check || !check.available) { throw new Error('Kein Update verfuegbar'); } // Stage: downloading const tmpDir = app.getPath('temp'); const installerPath = path.join(tmpDir, check.assetName); const res = await fetch(check.assetUrl, { method: 'GET', signal, redirect: 'follow' }); if (!res.ok) { throw new Error(`Download fehlgeschlagen: HTTP ${res.status}`); } const totalBytes = check.assetSize || 0; let downloadedBytes = 0; const chunks = []; const reader = res.body.getReader(); while (true) { if (signal.aborted) throw new Error('Abgebrochen'); const { done, value } = await reader.read(); if (done) break; chunks.push(value); downloadedBytes += value.length; if (onProgress) { onProgress({ stage: 'downloading', percent: totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : 0, bytesDownloaded: downloadedBytes, bytesTotal: totalBytes }); } } const fileBuffer = Buffer.concat(chunks); // Stage: verifying if (onProgress) onProgress({ stage: 'verifying', percent: 0 }); if (!verifyExeHeader(fileBuffer)) { throw new Error('Heruntergeladene Datei ist keine gueltige EXE'); } // Optional SHA-512 verification from latest.yml const expectedSha = await parseLatestYml(check.latestYmlUrl); if (expectedSha) { const actualSha = crypto.createHash('sha512').update(fileBuffer).digest('base64'); if (actualSha !== expectedSha) { // Try hex comparison const actualHex = crypto.createHash('sha512').update(fileBuffer).digest('hex'); if (actualHex !== expectedSha.toLowerCase()) { throw new Error('SHA-512 Pruefung fehlgeschlagen'); } } } // Write to disk fs.writeFileSync(installerPath, fileBuffer); // Stage: launching if (onProgress) onProgress({ stage: 'launching', percent: 100 }); const { spawn } = require('child_process'); spawn(installerPath, ['/S', '--updated', '--force-run'], { detached: true, stdio: 'ignore' }).unref(); // Stage: done if (onProgress) onProgress({ stage: 'done', percent: 100 }); setTimeout(() => app.quit(), 900); } catch (err) { if (onProgress) onProgress({ stage: 'error', error: err.message }); throw err; } finally { activeAbort = null; } } function abortUpdate() { if (activeAbort) { activeAbort.abort(); activeAbort = null; } } module.exports = { checkForUpdate, installUpdate, abortUpdate };