feat(torrent): api.js Fastify routes + stream mount (ref #917)

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-28 15:07:46 +02:00
parent 7aaa8d5409
commit fd6809b873
4 changed files with 114 additions and 6 deletions

View File

@ -0,0 +1,59 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
// SecuBox-Deb :: secubox-torrent :: api.js — CyberMind https://cybermind.fr
//
// Fastify control API: torrent lifecycle (add/list/keep/remove) + status/health,
// plus the /stream mount that hands off to the Range streaming handler.
import Fastify from 'fastify';
import path from 'node:path';
import { handleStream } from './stream.js';
export function buildApi({ engine, library, diskFreeBytes }) {
const app = Fastify({ logger: false });
app.get('/api/v1/torrent/health', async () => ({ status: 'ok' }));
app.get('/api/v1/torrent/status', async () => ({
active: engine.client.torrents.length, disk_free: diskFreeBytes(), webrtc: engine.webrtc !== false }));
app.post('/api/v1/torrent/add', async (req, reply) => {
const magnet = (req.body || {}).magnet;
if (!magnet) return reply.code(400).send({ error: 'magnet required' });
let meta;
try { meta = await engine.add(magnet); }
catch (e) { return reply.code(504).send({ error: e.message }); }
library.add({ infohash: meta.infohash, name: meta.name, magnet,
path: path.join(engine.downloadDir, 'tmp', meta.infohash) });
return meta;
});
app.get('/api/v1/torrent/list', async () =>
library.list().map(r => ({ ...r, stats: engine.stats(r.infohash) })));
app.get('/api/v1/torrent/files/:infohash', async (req, reply) => {
const t = engine.get(req.params.infohash);
if (!t) return reply.code(404).send({ error: 'not found' });
return t.files.map((f, i) => ({ idx: i, name: f.name, length: f.length }));
});
app.post('/api/v1/torrent/keep/:infohash', async (req, reply) => {
const ih = req.params.infohash;
if (!library.get(ih)) return reply.code(404).send({ error: 'not found' });
library.keep(ih, path.join(engine.downloadDir, 'library', ih));
return { status: 'kept' };
});
app.post('/api/v1/torrent/remove/:infohash', async (req) => {
engine.remove(req.params.infohash, { deleteData: true });
library.remove(req.params.infohash);
return { status: 'removed' };
});
app.get('/stream/:infohash/:fileIdx', (req, reply) => {
library.touch(req.params.infohash);
return handleStream(engine, req, reply.raw)
.then(() => { reply.hijack(); });
});
return app;
}

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
// SecuBox-Deb :: secubox-torrent :: api.test.js — CyberMind https://cybermind.fr
//
// Unit tests for the Fastify control API. Uses fastify .inject (no network),
// the Engine with FakeWebTorrent, and an in-memory Library.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildApi } from './api.js';
import { Library } from './library.js';
import { Engine } from './engine.js';
import { FakeWebTorrent } from './fakes.js';
function build() {
const engine = new Engine({ WebTorrentCtor: FakeWebTorrent, downloadDir: '/tmp', maxActive: 5, webrtc: true });
const library = new Library(':memory:');
return buildApi({ engine, library, diskFreeBytes: () => 10 * 1e9 });
}
test('health returns ok', async () => {
const app = build();
const r = await app.inject({ method: 'GET', url: '/api/v1/torrent/health' });
assert.equal(r.statusCode, 200);
assert.equal(r.json().status, 'ok');
});
test('add then list returns the torrent', async () => {
const app = build();
await app.inject({ method: 'POST', url: '/api/v1/torrent/add',
payload: { magnet: 'magnet:?xt=urn:btih:' + 'a'.repeat(40) } });
const r = await app.inject({ method: 'GET', url: '/api/v1/torrent/list' });
assert.equal(r.json().length, 1);
assert.equal(r.json()[0].kept, 0);
});
test('keep flips kept=1', async () => {
const app = build();
await app.inject({ method: 'POST', url: '/api/v1/torrent/add',
payload: { magnet: 'magnet:?xt=urn:btih:' + 'a'.repeat(40) } });
const r = await app.inject({ method: 'POST', url: '/api/v1/torrent/keep/' + 'a'.repeat(40) });
assert.equal(r.statusCode, 200);
const list = await app.inject({ method: 'GET', url: '/api/v1/torrent/list' });
assert.equal(list.json()[0].kept, 1);
});

View File

@ -10,7 +10,7 @@
"dependencies": {
"@roamhq/wrtc": "^0.8.0",
"better-sqlite3": "^11.3.0",
"fastify": "^4.28.1",
"fastify": "^4.29.1",
"webtorrent": "^2.5.1"
},
"engines": {

View File

@ -3,12 +3,16 @@
"version": "2.0.0",
"private": true,
"type": "module",
"engines": { "node": ">=18" },
"scripts": { "test": "node --test" },
"engines": {
"node": ">=18"
},
"scripts": {
"test": "node --test"
},
"dependencies": {
"webtorrent": "^2.5.1",
"@roamhq/wrtc": "^0.8.0",
"fastify": "^4.28.1",
"better-sqlite3": "^11.3.0"
"better-sqlite3": "^11.3.0",
"fastify": "^4.29.1",
"webtorrent": "^2.5.1"
}
}