chore(ci): instalar binarios y documentar dependencias

- Añade sección en README.md con instrucciones para p7zip (7z) y chdman
- Actualiza .gitea/workflows/ci.yaml para intentar instalar p7zip-full y mame-tools/mame (continue-on-error)
- Ajusta importService para validar ruta y pasar logger desde la ruta de import
This commit is contained in:
2026-02-09 18:19:45 +01:00
parent 4298b003d9
commit 12636aefc3
9 changed files with 276 additions and 9 deletions

View File

@@ -1,18 +1,18 @@
import { FastifyInstance } from 'fastify';
import { runner } from '../jobs/importRunner';
import { importDirectory } from '../services/importService';
export default async function importRoutes(app: FastifyInstance) {
app.post('/import/scan', async (request, reply) => {
const body = request.body as any;
// Encolar el job en background (placeholder)
// Encolar el job en background
setImmediate(() => {
// placeholder task: no persistencia, trabajo ligero en background
runner
.enqueue(async () => {
// usar body en caso necesario; aquí sólo un placeholder
void body;
return true;
// no await here; background task. Pasamos el logger de Fastify para
// que los mensajes de advertencia se integren con el sistema de logs.
return importDirectory({ dir: body?.dir, persist: body?.persist }, app.log);
})
.catch((err) => {
app.log.warn({ err }, 'Background import task failed');

View File

@@ -1,3 +1,16 @@
/**
* Servicio: checksumService
*
* Calcula sumas y metadatos de un fichero de forma eficiente usando streams.
* Las funciones principales procesan el archivo en streaming para producir:
* - `md5` (hex)
* - `sha1` (hex)
* - `crc32` (hex, 8 caracteres)
* - `size` (bytes)
*
* `computeHashes(filePath)` devuelve un objeto con los valores anteriores y
* está pensado para usarse durante la importación/normalización de ROMs.
*/
import fs from 'fs';
import { createHash } from 'crypto';

View File

@@ -1,3 +1,15 @@
/**
* Servicio: datVerifier
*
* Encargado de parsear ficheros DAT (XML de listas de ROMs) y de verificar si
* un ROM (por tamaño / CRC / MD5 / SHA1) coincide con una entrada del DAT.
*
* Actualmente este archivo contiene stubs mínimos: `parseDat` y
* `verifyRomAgainstDat` devuelven valores vacíos para permitir que las pruebas
* unitarias/integración opcionales se salten cuando `INTEGRATION` no está set.
* En fases posteriores se sustituirá por un parseador XML completo y lógica de
* matching detallada.
*/
export function parseDat(_xml: string): any {
// Stub: el parseo completo no se implementa en esta fase.
return {};

View File

@@ -1,3 +1,15 @@
/**
* Servicio: fsScanner
*
* Este módulo proporciona la función `scanDirectory` que recorre recursivamente
* un directorio (ignorando archivos y carpetas que comienzan por `.`) y devuelve
* una lista de metadatos de ficheros encontrados. Cada entrada contiene el
* `path` completo, `filename`, `name`, `size`, `format` detectado por extensión
* y `isArchive` indicando si se trata de un contenedor (zip/7z/chd).
*
* Se usa desde el importService para listar las ROMs/archivos que deben ser
* procesados/hashed y persistidos.
*/
import path from 'path';
import { promises as fsPromises } from 'fs';
import { detectFormat } from '../lib/fileTypeDetector';

View File

@@ -0,0 +1,111 @@
/**
* Servicio: importService
*
* Orquesta el proceso de importación de ROMs desde un directorio:
* 1. Lista archivos usando `scanDirectory`.
* 2. Calcula hashes y tamaño con `computeHashes` (streaming).
* 3. Normaliza el nombre a un `slug` y, si `persist` es true, crea/obtiene
* el `Game` correspondiente y hace `upsert` del `RomFile` en Prisma.
*
* `importDirectory` devuelve un resumen con contadores `{ processed, createdCount, upserted }`.
*/
import path from 'path';
import { promises as fsPromises } from 'fs';
import { scanDirectory } from './fsScanner';
import { computeHashes } from './checksumService';
import prisma from '../plugins/prisma';
/**
* Crea un `slug` a partir de un nombre legible. Usado para generar slugs
* por defecto al crear entradas `Game` cuando no existe la coincidencia.
*/
export function createSlug(name: string): string {
return name
.toString()
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export async function importDirectory(
options?: { dir?: string; persist?: boolean },
logger: { warn?: (msg: any, ...args: any[]) => void } = console
) {
const providedDir = options?.dir;
const dir = providedDir ?? process.env.ROMS_PATH ?? path.join(process.cwd(), 'roms');
const persist = options?.persist !== undefined ? options.persist : true;
// Si no se pasó explícitamente la ruta, validamos que exista y sea un directorio
if (!providedDir) {
try {
const stat = await fsPromises.stat(dir);
if (!stat.isDirectory()) {
logger.warn?.({ dir }, 'importDirectory: ruta no es un directorio');
return { processed: 0, createdCount: 0, upserted: 0 };
}
} catch (err) {
logger.warn?.({ err, dir }, 'importDirectory: ruta no accesible, abortando import');
return { processed: 0, createdCount: 0, upserted: 0 };
}
}
let files: any[] = [];
try {
files = await scanDirectory(dir as string);
} catch (err) {
logger.warn?.({ err, dir }, 'importDirectory: error listando directorio');
return { processed: 0, createdCount: 0, upserted: 0 };
}
let processed = 0;
let createdCount = 0;
let upserted = 0;
for (const file of files) {
processed++;
try {
const hashes = await computeHashes(file.path);
const checksum = hashes.md5;
const size = hashes.size;
const baseName = path.parse(file.filename).name;
const slug = createSlug(baseName);
let game = null;
if (persist) {
game = await prisma.game.findUnique({ where: { slug } });
if (!game) {
game = await prisma.game.create({ data: { title: baseName, slug } });
createdCount++;
}
await prisma.romFile.upsert({
where: { checksum },
update: { lastSeenAt: new Date(), size, hashes: JSON.stringify(hashes) },
create: {
path: file.path,
filename: file.filename,
checksum,
size,
format: file.format,
hashes: JSON.stringify(hashes),
gameId: game?.id,
},
});
upserted++;
}
} catch (err) {
logger.warn?.({ err, file }, 'importDirectory: error procesando fichero, se continúa con el siguiente');
continue;
}
}
return { processed, createdCount, upserted };
}
export default importDirectory;