RDM-Import, App-Designs, Auto-Update über Gitea, Logo; SFTP-Namensspalte repariert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 15:04:19 +02:00
co-authored by Claude Opus 5.5
parent a5eccc7764
commit 972fe0d316
16 changed files with 650 additions and 46 deletions
+141
View File
@@ -0,0 +1,141 @@
// Auto-Update über Gitea/Forgejo-Releases (z. B. https://git.mrblake.cc/MrBlake/MrTerm).
// Erwartete Release-Assets: *Setup*.exe (NSIS), *portable*.exe, *.AppImage, *.pacman
const { app } = require('electron');
const { spawn } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
const DEFAULT_REPO = 'https://git.mrblake.cc/MrBlake/MrTerm';
function parseRepo(url) {
const u = new URL((url || DEFAULT_REPO).replace(/\.git$/, '').replace(/\/+$/, ''));
const [owner, repo] = u.pathname.split('/').filter(Boolean);
if (!owner || !repo) throw new Error('Ungültige Repository-URL: ' + url);
return { base: u.origin, owner, repo };
}
function cmpVersion(a, b) {
const pa = String(a).replace(/^v/i, '').split(/[.-]/).map((x) => parseInt(x, 10) || 0);
const pb = String(b).replace(/^v/i, '').split(/[.-]/).map((x) => parseInt(x, 10) || 0);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
}
return 0;
}
// Welche Paketart läuft gerade?
function installKind() {
if (!app.isPackaged) return 'dev';
if (process.platform === 'win32') return process.env.PORTABLE_EXECUTABLE_FILE ? 'portable' : 'nsis';
if (process.env.APPIMAGE) return 'appimage';
return 'pacman';
}
function pickAsset(assets, kind) {
const by = (fn) => assets.find((a) => fn(a.name.toLowerCase()));
switch (kind) {
case 'nsis': return by((n) => n.endsWith('.exe') && n.includes('setup')) || by((n) => n.endsWith('.exe') && !n.includes('portable'));
case 'portable': return by((n) => n.endsWith('.exe') && n.includes('portable')) || by((n) => n.endsWith('.exe'));
case 'appimage': return by((n) => n.endsWith('.appimage'));
case 'pacman': return by((n) => n.endsWith('.pacman') || n.endsWith('.pkg.tar.zst')) || by((n) => n.endsWith('.appimage'));
default: return process.platform === 'win32' ? by((n) => n.endsWith('.exe')) : by((n) => n.endsWith('.appimage'));
}
}
class Updater {
constructor(store, send) {
this.store = store;
this.send = send;
this.latest = null;
this.file = null;
}
headers() {
const t = this.store.get().settings.updateToken;
return { Accept: 'application/json', ...(t ? { Authorization: `token ${t}` } : {}) };
}
async check() {
const { base, owner, repo } = parseRepo(this.store.get().settings.updateUrl);
const res = await fetch(`${base}/api/v1/repos/${owner}/${repo}/releases?limit=20`, { headers: this.headers() });
if (res.status === 404) throw new Error('Repository nicht gefunden (privat? Dann Zugriffstoken in den Einstellungen hinterlegen).');
if (!res.ok) throw new Error(`Update-Server antwortet mit ${res.status}`);
const allowPre = this.store.get().settings.updatePrerelease;
const rel = (await res.json()).find((r) => !r.draft && (allowPre || !r.prerelease));
const current = app.getVersion();
if (!rel) return { current, available: false };
const kind = installKind();
const asset = pickAsset(rel.assets || [], kind);
this.latest = { version: rel.tag_name.replace(/^v/i, ''), notes: rel.body || '', url: rel.html_url, asset, kind };
return {
current,
available: cmpVersion(rel.tag_name, current) > 0,
version: this.latest.version,
notes: this.latest.notes,
url: rel.html_url,
asset: asset ? { name: asset.name, size: asset.size } : null,
kind,
};
}
async download() {
if (!this.latest) await this.check();
const { asset } = this.latest;
if (!asset) throw new Error('Kein passendes Paket für dieses System im Release gefunden.');
const dir = path.join(os.tmpdir(), 'mrterm-update');
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, asset.name);
const res = await fetch(asset.browser_download_url, { headers: { ...this.headers(), Accept: 'application/octet-stream' } });
if (!res.ok) throw new Error(`Download fehlgeschlagen (${res.status})`);
const total = Number(res.headers.get('content-length')) || asset.size || 0;
const out = fs.createWriteStream(file + '.part');
let done = 0, last = 0;
for await (const chunk of res.body) {
out.write(chunk);
done += chunk.length;
if (Date.now() - last > 150) { last = Date.now(); this.send('update:progress', done, total); }
}
await new Promise((r) => out.end(r));
fs.renameSync(file + '.part', file);
this.send('update:progress', total, total);
this.file = file;
return file;
}
async install() {
if (!this.file) await this.download();
const file = this.file;
const kind = this.latest.kind;
if (kind === 'nsis') {
// Stilles Update, danach App automatisch neu starten
spawn(file, ['/S', '--force-run'], { detached: true, stdio: 'ignore' }).unref();
setTimeout(() => app.quit(), 300);
} else if (kind === 'portable') {
const target = path.join(path.dirname(process.env.PORTABLE_EXECUTABLE_FILE), path.basename(file));
fs.copyFileSync(file, target);
spawn(target, [], { detached: true, stdio: 'ignore' }).unref();
setTimeout(() => app.quit(), 300);
} else if (kind === 'appimage') {
const target = process.env.APPIMAGE;
const tmp = target + '.new';
fs.copyFileSync(file, tmp);
fs.chmodSync(tmp, 0o755);
fs.renameSync(tmp, target);
app.relaunch({ execPath: target });
app.exit(0);
} else if (kind === 'pacman' && /\.(pacman|pkg\.tar\.zst)$/.test(file)) {
await new Promise((resolve, reject) => {
const p = spawn('pkexec', ['pacman', '-U', '--noconfirm', file], { stdio: 'ignore' });
p.on('error', reject);
p.on('exit', (c) => (c === 0 ? resolve() : reject(new Error(`pacman beendet mit Code ${c}`))));
});
app.relaunch();
app.exit(0);
} else {
throw new Error('Automatische Installation hier nicht möglich. Datei liegt unter: ' + file);
}
}
}
module.exports = { Updater, DEFAULT_REPO };