Files
Git-Manager-Gui/updater.js

325 lines
12 KiB
JavaScript

// updater.js - Moderner Auto-Updater für Git Manager GUI
const { app, shell } = require('electron');
const https = require('https');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawn } = require('child_process');
const GITEA_API_URL = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Git-Manager-Gui/releases';
const TRUSTED_UPDATE_HOST = 'git.viper.ipv64.net';
class Updater {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.checkingForUpdates = false;
}
/**
* Hauptfunktion zur Prüfung auf Updates
*/
async checkForUpdates(silent = false) {
if (this.checkingForUpdates) return;
this.checkingForUpdates = true;
try {
const latestRelease = await this.getLatestRelease();
if (!latestRelease) return;
// Versionen säubern (nur Zahlen und Punkte)
const serverVer = latestRelease.tag_name.replace(/[^\d.]/g, '');
const localVer = app.getVersion().replace(/[^\d.]/g, '');
console.log(`[Updater] Version-Check: Server(${serverVer}) vs Lokal(${localVer})`);
if (this.compareVersions(serverVer, localVer) > 0) {
const asset = this.findAsset(latestRelease.assets);
const checksumAsset = this.findChecksumAsset(latestRelease.assets, asset);
const expectedSha256 = this.extractChecksumFromReleaseBody(latestRelease.body, asset?.name);
console.log("[Updater] Update verfügbar. Sende Daten an Renderer...");
this.mainWindow.webContents.send('update-available', {
version: serverVer,
body: latestRelease.body,
url: latestRelease.html_url,
asset: asset ? {
...asset,
checksumUrl: checksumAsset ? checksumAsset.browser_download_url : null,
expectedSha256
} : null
});
} else {
console.log("[Updater] Anwendung ist auf dem neuesten Stand.");
if (!silent) {
this.mainWindow.webContents.send('update-not-available', {
version: localVer
});
}
}
} catch (error) {
console.error('[Updater] Fehler beim Update-Check:', error);
} finally {
this.checkingForUpdates = false;
}
}
async getLatestRelease() {
return new Promise((resolve, reject) => {
const options = { headers: { 'User-Agent': 'GitManager-GUI-Updater' } };
https.get(GITEA_API_URL, options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const releases = JSON.parse(data);
resolve(Array.isArray(releases) ? releases[0] : null);
} catch (e) { reject(e); }
});
}).on('error', reject);
});
}
compareVersions(v1, v2) {
const a = v1.split('.').map(Number);
const b = v2.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if ((a[i] || 0) > (b[i] || 0)) return 1;
if ((a[i] || 0) < (b[i] || 0)) return -1;
}
return 0;
}
findAsset(assets) {
if (!assets) return null;
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
return assets.find(a => {
const name = String(a?.name || '').toLowerCase();
const validName = /^[a-z0-9._-]+$/.test(name);
return validName && name.endsWith(ext);
});
}
findChecksumAsset(assets, targetAsset) {
if (!Array.isArray(assets) || !targetAsset?.name) return null;
const targetLower = String(targetAsset.name).toLowerCase();
const exactCandidates = [
`${targetLower}.sha256`,
`${targetLower}.sha256sum`,
`${targetLower}.sha512`,
`${targetLower}.sha512sum`
];
const exact = assets.find(a => exactCandidates.includes(String(a?.name || '').toLowerCase()));
if (exact) return exact;
return assets.find(a => {
const name = String(a?.name || '').toLowerCase();
return name.includes('checksum') || name.includes('checksums') || name.endsWith('.sha256') || name.endsWith('.sha256sum');
}) || null;
}
extractChecksumFromReleaseBody(body, fileName) {
const text = String(body || '');
const target = String(fileName || '').trim();
if (!text || !target) return null;
const lines = text.split(/\r?\n/);
const targetLower = target.toLowerCase();
for (const line of lines) {
const normalized = String(line || '').trim();
if (!normalized) continue;
const match = normalized.match(/\b([a-fA-F0-9]{64})\b/);
if (!match) continue;
if (normalized.toLowerCase().includes(targetLower)) {
return match[1].toLowerCase();
}
}
return null;
}
downloadText(url) {
return new Promise((resolve, reject) => {
if (!this.isTrustedDownloadUrl(url)) {
reject(new Error('Unsichere Checksum-URL blockiert.'));
return;
}
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
return resolve(this.downloadText(res.headers.location || ''));
}
if (res.statusCode !== 200) {
reject(new Error(`Checksum-Download fehlgeschlagen: HTTP ${res.statusCode}`));
return;
}
let data = '';
res.on('data', chunk => data += chunk.toString('utf8'));
res.on('end', () => resolve(data));
}).on('error', reject);
});
}
extractChecksumFromText(text, fileName) {
const lines = String(text || '').split(/\r?\n/);
const targetLower = String(fileName || '').toLowerCase();
for (const line of lines) {
const normalized = String(line || '').trim();
if (!normalized) continue;
const hashMatch = normalized.match(/\b([a-fA-F0-9]{64})\b/);
if (!hashMatch) continue;
if (!targetLower || normalized.toLowerCase().includes(targetLower)) {
return hashMatch[1].toLowerCase();
}
}
return null;
}
computeFileSha256(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('error', reject);
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex').toLowerCase()));
});
}
async resolveExpectedSha256(asset) {
const expectedFromAsset = String(asset?.expectedSha256 || '').trim().toLowerCase();
if (/^[a-f0-9]{64}$/.test(expectedFromAsset)) return expectedFromAsset;
const checksumUrl = String(asset?.checksumUrl || '').trim();
if (!checksumUrl) return null;
const checksumText = await this.downloadText(checksumUrl);
return this.extractChecksumFromText(checksumText, asset?.name);
}
isTrustedDownloadUrl(rawUrl) {
try {
const parsed = new URL(String(rawUrl || ''));
return parsed.protocol === 'https:' && parsed.hostname === TRUSTED_UPDATE_HOST;
} catch (_) {
return false;
}
}
/**
* Startet den Download und führt die Installation aus
*/
async startDownload(asset) {
if (!asset || !asset.browser_download_url) {
console.error("[Updater] Kein gültiges Asset gefunden!");
return;
}
if (!this.isTrustedDownloadUrl(asset.browser_download_url)) {
console.error('[Updater] Unsichere Download-URL blockiert.');
return;
}
const tempPath = path.join(app.getPath('temp'), asset.name);
console.log(`[Updater] Download gestartet: ${asset.name} -> ${tempPath}`);
let expectedSha256 = null;
try {
expectedSha256 = await this.resolveExpectedSha256(asset);
} catch (e) {
console.error('[Updater] Konnte erwartete Checksumme nicht laden:', e?.message || e);
return;
}
if (!expectedSha256) {
console.error('[Updater] Kein SHA-256-Checksum-Wert gefunden. Update wurde aus Sicherheitsgruenden blockiert.');
return;
}
const file = fs.createWriteStream(tempPath);
const download = (url) => {
if (!this.isTrustedDownloadUrl(url)) {
console.error('[Updater] Unsicherer Redirect/Download blockiert.');
fs.unlink(tempPath, () => {});
return;
}
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
// Handle Redirects
if (res.statusCode === 301 || res.statusCode === 302) {
return download(res.headers.location || '');
}
if (res.statusCode !== 200) {
console.error(`[Updater] Download-Fehler: Status ${res.statusCode}`);
return;
}
res.pipe(file);
file.on('finish', async () => {
file.close();
try {
const actualSha256 = await this.computeFileSha256(tempPath);
if (actualSha256 !== expectedSha256) {
console.error('[Updater] Checksum-Validierung fehlgeschlagen. Installation wurde blockiert.');
fs.unlink(tempPath, () => {});
return;
}
} catch (verifyErr) {
console.error('[Updater] Checksum-Validierung konnte nicht ausgeführt werden:', verifyErr?.message || verifyErr);
fs.unlink(tempPath, () => {});
return;
}
console.log("[Updater] Download und Checksum-Validierung abgeschlossen. Initialisiere entkoppelten Installer...");
this.installAndQuit(tempPath);
});
}).on('error', (err) => {
fs.unlink(tempPath, () => {});
console.error("[Updater] Netzwerkfehler beim Download:", err);
});
};
download(asset.browser_download_url);
}
/**
* Führt den Installer entkoppelt aus und schließt die App mit Verzögerung
*/
installAndQuit(filePath) {
console.log(`[Updater] Bereite Installation vor: ${filePath}`);
if (process.platform === 'win32') {
try {
const child = spawn(filePath, [], {
detached: true,
stdio: 'ignore',
shell: false
});
child.unref();
console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek...");
// WICHTIG: Wir geben Windows Zeit, den Installer stabil zu laden,
// bevor wir die App schließen und die Dateisperren aufheben.
setTimeout(() => {
app.quit();
}, 2000);
} catch (err) {
console.error("[Updater] Kritischer Fehler beim Installer-Start:", err);
// Notfall-Versuch über shell
shell.openPath(filePath).then(() => app.quit());
}
} else {
// Linux/AppImage
shell.openPath(filePath).then(() => {
setTimeout(() => app.quit(), 1000);
});
}
}
}
module.exports = Updater;