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:
@@ -7,6 +7,7 @@ const { utils: sshUtils } = require('ssh2');
|
||||
const { Store } = require('./store');
|
||||
const { SshManager } = require('./ssh');
|
||||
const rdp = require('./rdp');
|
||||
const { Updater } = require('./updater');
|
||||
|
||||
let win;
|
||||
const store = new Store();
|
||||
@@ -42,6 +43,7 @@ function askSecret(sessionId, req) {
|
||||
}
|
||||
|
||||
const ssh = new SshManager(store, confirmHostKey, askSecret);
|
||||
const updater = new Updater(store, send);
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
@@ -50,6 +52,7 @@ function createWindow() {
|
||||
minWidth: 900,
|
||||
minHeight: 560,
|
||||
backgroundColor: '#14161d',
|
||||
icon: path.join(__dirname, '..', '..', 'assets', 'icon.png'),
|
||||
frame: false,
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
|
||||
webPreferences: {
|
||||
@@ -241,6 +244,46 @@ handle('rdp:launch', (hostId) => {
|
||||
return r.client;
|
||||
});
|
||||
|
||||
// ---------- Import aus anderen Programmen (z. B. Devolutions RDM) ----------
|
||||
handle('import:pickFile', async (title, extensions) => {
|
||||
const r = await dialog.showOpenDialog(win, { title, filters: [{ name: title, extensions }, { name: 'Alle Dateien', extensions: ['*'] }], properties: ['openFile'] });
|
||||
if (r.canceled) return null;
|
||||
const buf = fs.readFileSync(r.filePaths[0]);
|
||||
// UTF-16-Exporte (BOM) erkennen
|
||||
const text = buf[0] === 0xff && buf[1] === 0xfe ? buf.toString('utf16le') : buf.toString('utf8');
|
||||
return { name: path.basename(r.filePaths[0]), content: text.replace(/^/, '') };
|
||||
});
|
||||
// entries: [{ folder: ['A','B'], host?: {...} }] – Ordner werden per Pfad angelegt bzw. wiederverwendet
|
||||
handle('vault:bulkImport', (entries) => {
|
||||
const groups = store.get().groups;
|
||||
const ensure = (parts) => {
|
||||
let parent = null;
|
||||
for (const label of parts) {
|
||||
let g = groups.find((x) => (x.parentId || null) === parent && x.label.toLowerCase() === label.toLowerCase());
|
||||
if (!g) { g = { id: crypto.randomUUID(), label, parentId: parent, createdAt: Date.now() }; groups.push(g); }
|
||||
parent = g.id;
|
||||
}
|
||||
return parent;
|
||||
};
|
||||
let hosts = 0, skipped = 0;
|
||||
for (const e of entries) {
|
||||
const groupId = ensure(e.folder || []);
|
||||
if (!e.host) continue;
|
||||
const dup = store.get().hosts.find((h) => h.address === e.host.address && (h.groupId || null) === groupId && (h.label || '') === (e.host.label || '') && (h.protocol || 'ssh') === e.host.protocol);
|
||||
if (dup) { skipped++; continue; }
|
||||
store.get().hosts.push({ ...e.host, id: crypto.randomUUID(), groupId, createdAt: Date.now(), updatedAt: Date.now() });
|
||||
hosts++;
|
||||
}
|
||||
store.save();
|
||||
return { hosts, skipped, groups: groups.length };
|
||||
});
|
||||
|
||||
// ---------- Updates ----------
|
||||
handle('update:check', () => updater.check());
|
||||
handle('update:download', () => updater.download());
|
||||
handle('update:install', () => updater.install());
|
||||
handle('app:version', () => app.getVersion());
|
||||
|
||||
handle('clipboard:write', (t) => clipboard.writeText(t));
|
||||
handle('clipboard:read', () => clipboard.readText());
|
||||
handle('shell:open', (url) => shell.openExternal(url));
|
||||
@@ -248,6 +291,9 @@ handle('shell:open', (url) => shell.openExternal(url));
|
||||
app.whenReady().then(() => {
|
||||
store.load();
|
||||
createWindow();
|
||||
if (store.get().settings.updateAutoCheck && app.isPackaged) {
|
||||
setTimeout(() => updater.check().then((r) => { if (r.available) send('update:available', r); }).catch(() => {}), 6000);
|
||||
}
|
||||
});
|
||||
app.on('window-all-closed', () => { ssh.closeAll(); app.quit(); });
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ const DEFAULTS = {
|
||||
copyOnSelect: true,
|
||||
keepAlive: 30,
|
||||
rdpClientLinux: 'auto',
|
||||
appTheme: 'midnight',
|
||||
accent: '',
|
||||
updateUrl: 'https://git.mrblake.cc/MrBlake/MrTerm',
|
||||
updateToken: '',
|
||||
updateAutoCheck: true,
|
||||
updatePrerelease: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user