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:
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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';
|
||||
|
||||
111
backend/src/services/importService.ts
Normal file
111
backend/src/services/importService.ts
Normal 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;
|
||||
Reference in New Issue
Block a user