Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs. Pithos moves from a toolbar menu that opened a loopback tab to a left-sidebar panel. Existing installs get the same version over the extension update channel.
128 lines
5 KiB
JavaScript
128 lines
5 KiB
JavaScript
// Pithos for Theseus. The add-on runs the same control server as the web
|
|
// console and desktop app, in Theseus's main process, and shows the same UI
|
|
// as a left-sidebar panel. The panel is the add-on's own ui/index.html, so
|
|
// no loopback address ever appears in the address bar: the UI's API calls,
|
|
// uploads and live events travel over the add-on IPC bridge (invoke / emit),
|
|
// and this file forwards them to the server with an internal session.
|
|
//
|
|
// Loaded with require() by the add-on host; core/ is ESM, hence import().
|
|
|
|
const path = require("node:path");
|
|
const { pathToFileURL } = require("node:url");
|
|
|
|
let pithos = null;
|
|
let starting = null;
|
|
let cookie = null;
|
|
let events = null; // AbortController for the /api/events relay
|
|
|
|
module.exports = {
|
|
activate(api) {
|
|
// The pinned s3d release lands in the per-add-on data folder, never in the
|
|
// add-on folder itself (that one is replaced on update).
|
|
const dataDir = path.join(api.dataDir || path.join(api.folder, "..", "..", "extensions-data"), "pithos");
|
|
|
|
async function ensureServer() {
|
|
if (pithos) return pithos;
|
|
if (!starting) {
|
|
starting = (async () => {
|
|
const { createPithos } = await import(pathToFileURL(path.join(__dirname, "core", "server.js")).href);
|
|
const p = await createPithos({
|
|
host: "127.0.0.1",
|
|
port: 0,
|
|
hostName: "theseus",
|
|
binDir: path.join(dataDir, "bin"),
|
|
openExternal: (url) => api.openTab(url),
|
|
});
|
|
cookie = p.internalSession();
|
|
pithos = p;
|
|
api.log(`control server on ${p.url}`);
|
|
return p;
|
|
})().finally(() => { starting = null; });
|
|
}
|
|
return starting;
|
|
}
|
|
|
|
async function call(method, apiPath, { body, headers = {} } = {}) {
|
|
const p = await ensureServer();
|
|
const res = await fetch(new URL(apiPath, p.url), {
|
|
method,
|
|
headers: { cookie, "x-pithos": "1", ...headers },
|
|
body,
|
|
});
|
|
const text = await res.text();
|
|
let data = null;
|
|
try { data = text ? JSON.parse(text) : null; } catch { data = { error: text }; }
|
|
return { status: res.status, data };
|
|
}
|
|
|
|
// Relay the server's event stream to the panel as api.emit events.
|
|
async function startEvents() {
|
|
if (events) return;
|
|
const p = await ensureServer();
|
|
const ctl = new AbortController();
|
|
events = ctl;
|
|
try {
|
|
const res = await fetch(new URL("/api/events", p.url), { headers: { cookie }, signal: ctl.signal });
|
|
const reader = res.body.getReader();
|
|
const dec = new TextDecoder();
|
|
let buf = "";
|
|
for (;;) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buf += dec.decode(value, { stream: true });
|
|
let i;
|
|
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
const block = buf.slice(0, i);
|
|
buf = buf.slice(i + 2);
|
|
const ev = /^event: (.+)$/m.exec(block);
|
|
const data = /^data: (.+)$/m.exec(block);
|
|
if (ev && data) api.emit("pithos-event", { event: ev[1], data: JSON.parse(data[1]) });
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (!ctl.signal.aborted) api.log("event relay stopped:", e.message);
|
|
} finally {
|
|
if (events === ctl) events = null;
|
|
}
|
|
}
|
|
|
|
api.onMessage("api", ({ method, path: apiPath, body }) =>
|
|
call(method || "GET", apiPath, body === undefined ? {} : { body: JSON.stringify(body), headers: { "content-type": "application/json" } }));
|
|
|
|
// Uploads arrive as one ArrayBuffer over IPC; forward it as the PUT body.
|
|
api.onMessage("upload", ({ path: apiPath, bytes, type }) =>
|
|
call("PUT", apiPath, { body: Buffer.from(bytes), headers: { "content-type": type || "application/octet-stream" } }));
|
|
|
|
// Hand the file to Theseus's own download manager through a one-time link.
|
|
api.onMessage("download", async ({ path: apiPath }) => {
|
|
const p = await ensureServer();
|
|
const { session } = require("electron");
|
|
session.defaultSession.downloadURL(p.downloadUrl(apiPath));
|
|
return { ok: true };
|
|
});
|
|
|
|
api.onMessage("events-start", () => { startEvents(); return { ok: true }; });
|
|
api.onMessage("open-external", ({ url }) => {
|
|
if (!/^https:\/\//.test(url)) throw new Error("only https links");
|
|
api.openTab(url);
|
|
return { ok: true };
|
|
});
|
|
// A full browser-tab view, for anyone who prefers it to the sidebar.
|
|
api.onMessage("open-in-tab", async () => {
|
|
const p = await ensureServer();
|
|
api.openTab(p.authUrl);
|
|
return { ok: true };
|
|
});
|
|
|
|
api.registerSidebarPanel({ id: "main", title: "Pithos", page: "ui/index.html" });
|
|
|
|
// The host has no quit hook for add-ons; without this an s3d we started
|
|
// would outlive Theseus.
|
|
process.on("exit", () => { try { pithos && pithos.daemon.child && pithos.daemon.child.kill(); } catch {} });
|
|
},
|
|
|
|
async deactivate() {
|
|
if (events) { events.abort(); events = null; }
|
|
if (pithos) { const p = pithos; pithos = null; cookie = null; await p.close(); }
|
|
},
|
|
};
|