pithos/core/binary.js
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
3.6 KiB
JavaScript

// Installs the pinned s3d release into Pithos's own bin folder. The digests are
// GitHub's published sha256 for each release asset; a download that does not
// match is discarded before anything is written to the bin folder.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import zlib from 'node:zlib';
export const S3D_VERSION = '0.2.0';
const ASSETS = {
'win32-x64': { file: 's3d_windows_amd64.zip', sha256: '239356ef55228d03aa1889c7208357b83faf66947ade2cba4f7a3893c632c473' },
'linux-x64': { file: 's3d_linux_amd64.zip', sha256: 'e537d68be8b6749e3e239d572c497a4f1ebc81ef5a3b70d5ec3cc9ffa6498e73' },
'linux-arm64': { file: 's3d_linux_arm64.zip', sha256: '76848764f09922f31489c809514cdc928b3882836d6e5b64dcac085a84b4958b' },
'darwin-x64': { file: 's3d_darwin_amd64.zip', sha256: 'd7fe282efab35242b001ed9040d833334034682225b4c2b6b2eda67ecca8992e' },
'darwin-arm64': { file: 's3d_darwin_arm64.zip', sha256: '9eb6b73cb30b5b6b54fbbc583af6f7d9e56a30c55247ac70568918a1c98a3353' },
};
export function assetForPlatform() {
return ASSETS[`${process.platform}-${process.arch}`] || null;
}
export async function installS3d(binDir, onProgress = () => {}) {
const asset = assetForPlatform();
if (!asset) throw new Error(`no s3d release for ${process.platform}-${process.arch}`);
const url = `https://github.com/SiaFoundation/s3d/releases/download/v${S3D_VERSION}/${asset.file}`;
onProgress({ phase: 'download', url });
const res = await fetch(url, { redirect: 'follow' });
if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
const zip = Buffer.from(await res.arrayBuffer());
const digest = crypto.createHash('sha256').update(zip).digest('hex');
if (digest !== asset.sha256) throw new Error(`checksum mismatch for ${asset.file} (got ${digest})`);
onProgress({ phase: 'extract' });
const exe = process.platform === 'win32' ? 's3d.exe' : 's3d';
const data = extractFromZip(zip, (name) => path.basename(name) === exe);
if (!data) throw new Error(`${exe} not found in ${asset.file}`);
fs.mkdirSync(binDir, { recursive: true });
const target = path.join(binDir, exe);
const tmp = target + '.new';
fs.writeFileSync(tmp, data, { mode: 0o755 });
fs.renameSync(tmp, target);
onProgress({ phase: 'done', path: target });
return target;
}
// Minimal zip reader: walks the central directory and inflates one entry.
function extractFromZip(buf, match) {
let eocd = -1;
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) {
if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
}
if (eocd < 0) throw new Error('not a zip file');
const count = buf.readUInt16LE(eocd + 10);
let p = buf.readUInt32LE(eocd + 16);
for (let n = 0; n < count; n++) {
if (buf.readUInt32LE(p) !== 0x02014b50) throw new Error('corrupt zip directory');
const method = buf.readUInt16LE(p + 10);
const csize = buf.readUInt32LE(p + 20);
const nameLen = buf.readUInt16LE(p + 28);
const extraLen = buf.readUInt16LE(p + 30);
const commentLen = buf.readUInt16LE(p + 32);
const local = buf.readUInt32LE(p + 42);
const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
p += 46 + nameLen + extraLen + commentLen;
if (!match(name)) continue;
const lName = buf.readUInt16LE(local + 26);
const lExtra = buf.readUInt16LE(local + 28);
const start = local + 30 + lName + lExtra;
const raw = buf.subarray(start, start + csize);
if (method === 0) return Buffer.from(raw);
if (method === 8) return zlib.inflateRawSync(raw);
throw new Error(`unsupported zip method ${method}`);
}
return null;
}