pithos/desktop/main.cjs
Local Dev 6b8eeaf82b Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.

One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00

77 lines
2.5 KiB
JavaScript

// Pithos desktop: an Electron window over the same control server the web
// console runs. The server binds a random loopback port and the window signs
// in with the one-time secret, so nothing else on the machine can drive it.
const { app, BrowserWindow, shell, Menu, dialog } = require('electron');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
let pithos;
let win;
if (!app.requestSingleInstanceLock()) {
app.quit();
} else {
app.on('second-instance', () => {
if (win) { if (win.isMinimized()) win.restore(); win.focus(); }
});
app.whenReady().then(start).catch((err) => {
dialog.showErrorBox('Pithos failed to start', String(err?.stack || err));
app.quit();
});
}
async function start() {
const { createPithos } = await import(pathToFileURL(path.join(__dirname, '..', 'core', 'server.js')).href);
pithos = await createPithos({
host: '127.0.0.1',
port: 0,
hostName: 'desktop',
configFile: process.env.S3D_CONFIG_FILE || undefined,
// The pinned s3d release installs here, outside the (read-only) app bundle.
binDir: path.join(app.getPath('userData'), 'bin'),
openExternal: (url) => shell.openExternal(url),
});
Menu.setApplicationMenu(process.platform === 'darwin' ? Menu.buildFromTemplate([
{ role: 'appMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' },
]) : null);
win = new BrowserWindow({
width: 1180,
height: 800,
minWidth: 380,
minHeight: 500,
title: 'Pithos',
backgroundColor: '#0f1214',
autoHideMenuBar: true,
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
});
const origin = new URL(pithos.url).origin;
// Keep the window on the console; anything else goes to the system browser.
win.webContents.on('will-navigate', (e, url) => {
if (new URL(url).origin !== origin) {
e.preventDefault();
if (url.startsWith('https://')) shell.openExternal(url);
}
});
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https://')) shell.openExternal(url);
return { action: 'deny' };
});
win.loadURL(pithos.authUrl);
win.on('closed', () => { win = null; });
}
let quitting = false;
app.on('before-quit', (e) => {
if (quitting || !pithos) return;
// Stop s3d cleanly so its upload buffer and database are flushed to disk.
e.preventDefault();
quitting = true;
pithos.close().finally(() => app.quit());
});
app.on('window-all-closed', () => app.quit());