secubox-deb/packages/secubox-torrent/lxc/app/api.js
CyberMind-FR 252fb6825a fix(torrent): preserve kept flag on re-add, resume library at startup, honest paths (ref #917)
library.js add() used INSERT OR REPLACE, so re-adding an already-kept
magnet reset kept=0 and added_at — the ephemeral purge sweep would then
delete it 6h later as if it had never been kept. Switch to
INSERT ... ON CONFLICT DO UPDATE SET last_played_at only, preserving kept/
added_at/name/magnet/path across a re-add.

server.js never resynced the WebTorrent engine with the library DB at
startup: kept titles 404'd on /stream after any restart (engine.get()
returns null for a torrent the engine never re-learned about), and
runPurge's engine.remove() became a silent no-op for ephemeral rows,
leaking their downloaded bytes on disk while their DB rows vanished. Add
resumeLibrary(engine, library, { rmrf }), wired into start(): kept rows are
re-added to the engine (try/catch + promise .catch so one bad magnet can't
abort the loop); ephemeral rows are dropped from the library and their
download dir is reclaimed via an injectable rmrf.

api.js recorded a fictitious tmp/<infohash> (add) and library/<infohash>
(keep) path that no code ever moves files into — files download flat to
downloadDir. Record path.join(downloadDir, infohash) in both places so the
path resumeLibrary's rmrf acts on matches where files actually live.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-28 16:10:15 +02:00

60 lines
2.3 KiB
JavaScript

// 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, 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, 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;
}