const fs = require('fs-extra'); const path = require('path'); const { getCadInfo } = require('./router'); const CODE_FOLDER_REGEX = /^\d{3}$/; const MAX_SCAN_DEPTH = 6; const CACHE_FILENAME = 'dest-index.cache.json'; let _cacheDir = null; function setCacheDir(dir) { _cacheDir = dir; } function toCadKey(cadInfo) { return String(cadInfo?.key || '').toLowerCase(); } function getCacheFilePath() { return _cacheDir ? path.join(_cacheDir, CACHE_FILENAME) : null; } // ── Cache su disco (solo folderIndex — struttura cartelle) ───────────────── async function tryLoadFolderIndexCache(destinationRoot) { const cacheFile = getCacheFilePath(); if (!cacheFile) return null; try { const raw = await fs.readJson(cacheFile); if (raw?.destination !== destinationRoot) return null; return { folderIndex: new Map(raw.folderIndex || []), savedAt: raw.savedAt }; } catch { return null; } } async function saveFolderIndexCache(destinationRoot, folderIndex) { const cacheFile = getCacheFilePath(); if (!cacheFile) return; try { await fs.writeJson(cacheFile, { destination: destinationRoot, savedAt: new Date().toISOString(), folderIndex: [...folderIndex.entries()], }); } catch { // cache non critica } } async function clearCache() { const cacheFile = getCacheFilePath(); if (!cacheFile) return; try { await fs.remove(cacheFile); } catch { // ignora } } // ── Scansione completa (folderIndex + cadKeyIndex in un unico passaggio) ─── async function fullScan(destinationRoot, onProgress) { const folderIndex = new Map(); const cadKeyIndex = new Set(); if (!destinationRoot || !(await fs.pathExists(destinationRoot))) { return { folderIndex, cadKeyIndex }; } let topEntries; try { topEntries = await fs.readdir(destinationRoot, { withFileTypes: true }); } catch { return { folderIndex, cadKeyIndex }; } const topDirs = topEntries .filter((e) => e.isDirectory()) .map((e) => ({ name: e.name, fullPath: path.join(destinationRoot, e.name) })); const total = topDirs.length; let completed = 0; let scannedFiles = 0; for (const entry of topEntries) { if (!entry.isFile()) continue; const cadInfo = getCadInfo(entry.name); if (cadInfo) { scannedFiles += 1; cadKeyIndex.add(toCadKey(cadInfo)); } } async function walkSubtree(dirPath, depth) { if (depth > MAX_SCAN_DEPTH) return; let entries; try { entries = await fs.readdir(dirPath, { withFileTypes: true }); } catch { return; } const subdirPromises = []; for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { if (CODE_FOLDER_REGEX.test(entry.name)) { const rows = folderIndex.get(entry.name) || []; rows.push(fullPath); folderIndex.set(entry.name, rows); } subdirPromises.push(walkSubtree(fullPath, depth + 1)); } else if (entry.isFile()) { const cadInfo = getCadInfo(entry.name); if (cadInfo) { scannedFiles += 1; onProgress?.({ phase: 'index-dup', scanned: scannedFiles, file: entry.name }); cadKeyIndex.add(toCadKey(cadInfo)); } } } await Promise.all(subdirPromises); } await Promise.all( topDirs.map(async ({ name, fullPath }) => { onProgress?.({ phase: 'index-dest', current: completed, total, folder: name }); if (CODE_FOLDER_REGEX.test(name)) { const rows = folderIndex.get(name) || []; rows.push(fullPath); folderIndex.set(name, rows); } await walkSubtree(fullPath, 1); completed += 1; onProgress?.({ phase: 'index-dest', current: completed, total, folder: name }); }) ); return { folderIndex, cadKeyIndex }; } // ── Scansione veloce dei file (usa il folderIndex già noto dalla cache) ──── // // Invece di traversare l'intero albero, legge direttamente SOLO le cartelle // a 3 cifre già note. Questo riduce drasticamente le chiamate readdir su SMB. async function scanCadKeysFromFolderIndex(folderIndex, onProgress) { const cadKeyIndex = new Set(); const allFolderPaths = [...folderIndex.values()].flat(); const total = allFolderPaths.length; let completed = 0; await Promise.all( allFolderPaths.map(async (folderPath) => { for (const sub of ['A', 'M_B']) { const subPath = path.join(folderPath, sub); let entries; try { entries = await fs.readdir(subPath, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { if (!entry.isFile()) continue; const cadInfo = getCadInfo(entry.name); if (cadInfo) { cadKeyIndex.add(toCadKey(cadInfo)); } } } completed += 1; onProgress?.({ phase: 'index-dup', current: completed, total }); }) ); return cadKeyIndex; } // ── Entry point ──────────────────────────────────────────────────────────── async function isCacheStillValid(destinationRoot, cached) { // Controlla le cartelle top-level E le cartelle a 3 cifre al loro interno. // Costa 1 + N_top_level readdir — rileva nuove cartelle aggiunte da colleghi. let currentEntries; try { currentEntries = await fs.readdir(destinationRoot, { withFileTypes: true }); } catch { return false; } const currentDirNames = new Set( currentEntries.filter((e) => e.isDirectory()).map((e) => e.name) ); // Ricostruisce: topLevelDir → Set dalla cache const cachedGroupsByTopDir = new Map(); for (const [group, paths] of cached.folderIndex.entries()) { for (const p of paths) { const topDir = path.relative(destinationRoot, p).split(path.sep)[0]; if (!cachedGroupsByTopDir.has(topDir)) cachedGroupsByTopDir.set(topDir, new Set()); cachedGroupsByTopDir.get(topDir).add(group); } } const cachedDirNames = new Set(cachedGroupsByTopDir.keys()); // Controlla top-level for (const name of currentDirNames) { if (!cachedDirNames.has(name)) return false; } for (const name of cachedDirNames) { if (!currentDirNames.has(name)) return false; } // Controlla cartelle a 3 cifre dentro ogni top-level (solo nuove aggiunte) for (const topDir of currentDirNames) { let subEntries; try { subEntries = await fs.readdir(path.join(destinationRoot, topDir), { withFileTypes: true }); } catch { return false; } const cachedGroups = cachedGroupsByTopDir.get(topDir) || new Set(); for (const e of subEntries) { if (e.isDirectory() && CODE_FOLDER_REGEX.test(e.name) && !cachedGroups.has(e.name)) { return false; // nuova cartella a 3 cifre rilevata } } } return true; } async function buildDestinationIndexes(destinationRoot, onProgress) { if (!destinationRoot) { return { folderIndex: new Map(), cadKeyIndex: new Set() }; } const cached = await tryLoadFolderIndexCache(destinationRoot); if (cached && (await isCacheStillValid(destinationRoot, cached))) { // folderIndex dalla cache (istantaneo), cadKeyIndex sempre fresco onProgress?.({ phase: 'index-cache', savedAt: cached.savedAt, total: cached.folderIndex.size }); const cadKeyIndex = await scanCadKeysFromFolderIndex(cached.folderIndex, onProgress); return { folderIndex: cached.folderIndex, cadKeyIndex }; } // Prima volta, cache invalidata o struttura cambiata: scansione completa const result = await fullScan(destinationRoot, onProgress); await saveFolderIndexCache(destinationRoot, result.folderIndex); return result; } module.exports = { buildDestinationIndexes, setCacheDir, clearCache, toCadKey };