feat: expose archive entries in fsScanner
- Añade `scanDirectory` support para listar entradas internas de ZIP/7z - Añade test unitario que mockea `archiveReader.listArchiveEntries` - Añade límite configurable `ARCHIVE_MAX_ENTRIES` y comprobación básica de seguridad
This commit is contained in:
@@ -13,9 +13,13 @@
|
||||
import path from 'path';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { detectFormat } from '../lib/fileTypeDetector';
|
||||
import { listArchiveEntries } from './archiveReader';
|
||||
|
||||
const ARCHIVE_MAX_ENTRIES = Number(process.env.ARCHIVE_MAX_ENTRIES) || 1000;
|
||||
|
||||
export async function scanDirectory(dirPath: string): Promise<any[]> {
|
||||
const results: any[] = [];
|
||||
let archiveEntriesAdded = 0;
|
||||
|
||||
async function walk(dir: string) {
|
||||
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
||||
@@ -43,6 +47,35 @@ export async function scanDirectory(dirPath: string): Promise<any[]> {
|
||||
format,
|
||||
isArchive,
|
||||
});
|
||||
|
||||
if (isArchive) {
|
||||
try {
|
||||
const entries = await listArchiveEntries(full);
|
||||
|
||||
for (const e of entries) {
|
||||
if (archiveEntriesAdded >= ARCHIVE_MAX_ENTRIES) break;
|
||||
if (!e || !e.name) continue;
|
||||
// avoid path traversal or absolute paths
|
||||
if (e.name.includes('..') || path.isAbsolute(e.name)) continue;
|
||||
|
||||
results.push({
|
||||
path: `${full}::${e.name}`,
|
||||
containerPath: full,
|
||||
entryPath: e.name,
|
||||
filename: path.basename(e.name),
|
||||
name: e.name,
|
||||
size: e.size,
|
||||
format: detectFormat(e.name),
|
||||
isArchive: false,
|
||||
isArchiveEntry: true,
|
||||
});
|
||||
|
||||
archiveEntriesAdded++;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore archive listing errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
backend/tests/services/fsScanner.archiveEntries.spec.ts
Normal file
35
backend/tests/services/fsScanner.archiveEntries.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { promises as fs } from 'fs';
|
||||
import { afterEach, it, expect, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../src/services/archiveReader', () => ({ listArchiveEntries: vi.fn() }));
|
||||
|
||||
import scanDirectory from '../../src/services/fsScanner';
|
||||
import { listArchiveEntries } from '../../src/services/archiveReader';
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('expone entradas internas de archivos como items virtuales', async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fsScanner-test-'));
|
||||
const collectionFile = path.join(tmpDir, 'collection.zip');
|
||||
await fs.writeFile(collectionFile, '');
|
||||
|
||||
(listArchiveEntries as unknown as vi.Mock).mockResolvedValue([
|
||||
{ name: 'inner/rom1.bin', size: 1234 },
|
||||
]);
|
||||
|
||||
const results = await scanDirectory(tmpDir);
|
||||
|
||||
const expectedPath = `${collectionFile}::inner/rom1.bin`;
|
||||
const found = results.find((r: any) => r.path === expectedPath);
|
||||
|
||||
expect(found).toBeDefined();
|
||||
expect(found.isArchiveEntry).toBe(true);
|
||||
expect(found.containerPath).toBe(collectionFile);
|
||||
expect(found.entryPath).toBe('inner/rom1.bin');
|
||||
expect(found.filename).toBe('rom1.bin');
|
||||
expect(found.format).toBe('bin');
|
||||
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
Reference in New Issue
Block a user