feat(router): instrada in sottocartella A/M_B e ottimizza cache destinazione

- Router: aggiunge campo `subfolder` a getCadInfo (91xxxxx → A, 92xxxxx → M_B);
  getDestinationDecision appende la sottocartella al percorso finale
- Scanner (fast path): legge A e M_B dentro ogni cartella a 3 cifre per
  il rilevamento duplicati, anziché la cartella a 3 cifre stessa
- Cache: persistenza su disco del folderIndex; cadKeyIndex sempre fresco
- isCacheStillValid: validazione a due livelli (top-level + cartelle a 3
  cifre) — rileva automaticamente nuove cartelle aggiunte da colleghi
- UI: rimosso pulsante "Riscansiona destinazione"; gestione cache completamente
  automatica e trasparente per l'utente
This commit is contained in:
2026-03-16 16:17:24 +01:00
parent a49d338741
commit 65b1777a60
4 changed files with 204 additions and 23 deletions

View File

@@ -4,21 +4,64 @@ 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();
}
/**
* Scansiona la cartella di destinazione in un unico passaggio parallelo e
* costruisce contemporaneamente:
* - folderIndex: Map<group3digits, fullPath[]> (per il routing)
* - cadKeyIndex: Set<cadKey> (per il rilevamento duplicati)
*
* Legge prima le sottocartelle di primo livello (1 sola readdir) per ottenere
* il totale e mostrare una barra determinata durante la scansione.
*/
async function buildDestinationIndexes(destinationRoot, onProgress) {
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();
@@ -26,7 +69,6 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
return { folderIndex, cadKeyIndex };
}
// Lettura di primo livello: 1 sola chiamata per avere il totale
let topEntries;
try {
topEntries = await fs.readdir(destinationRoot, { withFileTypes: true });
@@ -42,7 +84,6 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
let completed = 0;
let scannedFiles = 0;
// Registra i file CAD al primo livello (non cartelle)
for (const entry of topEntries) {
if (!entry.isFile()) continue;
const cadInfo = getCadInfo(entry.name);
@@ -52,11 +93,8 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
}
}
// Walk ricorsivo del sottoalbero di una cartella di primo livello
async function walkSubtree(dirPath, depth) {
if (depth > MAX_SCAN_DEPTH) {
return;
}
if (depth > MAX_SCAN_DEPTH) return;
let entries;
try {
@@ -90,7 +128,6 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
await Promise.all(subdirPromises);
}
// Processa tutte le cartelle di primo livello in parallelo
await Promise.all(
topDirs.map(async ({ name, fullPath }) => {
onProgress?.({ phase: 'index-dest', current: completed, total, folder: name });
@@ -111,4 +148,117 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
return { folderIndex, cadKeyIndex };
}
module.exports = { buildDestinationIndexes, toCadKey };
// ── 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<group> 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 };

View File

@@ -16,6 +16,9 @@ function getCadInfo(filename) {
const version = match[3] || '';
const digits = rawCode.replace(/\D/g, '');
const routingGroup = digits.length >= 5 ? digits.slice(2, 5) : null;
const subfolder = digits.length >= 2
? (digits[1] === '1' ? 'A' : digits[1] === '2' ? 'M_B' : null)
: null;
return {
code: rawCode,
@@ -24,6 +27,7 @@ function getCadInfo(filename) {
version,
key: `${rawCode}.${type}`,
routingGroup,
subfolder,
};
}
@@ -57,7 +61,14 @@ function getDestinationDecision(filename, config, destinationIndex) {
};
}
return { destination: candidates[0], reason: null };
if (!cadInfo.subfolder) {
return {
destination: null,
reason: `Prefisso non riconosciuto (${cadInfo.digits[1]}): atteso 91 (→ A) o 92 (→ M_B)`,
};
}
return { destination: path.join(candidates[0], cadInfo.subfolder), reason: null };
}
function findDestination(filename, config, destinationIndex) {