diff --git a/main.js b/main.js index c693662..c25baf1 100644 --- a/main.js +++ b/main.js @@ -4,6 +4,7 @@ const fs = require('fs-extra'); const { processFolder } = require('./services/folderProcessor'); const { processZip } = require('./services/zipProcessor'); +const { setCacheDir, clearCache } = require('./services/destinationScanner'); const { resolveUnroutedDir, listUnroutedFiles, @@ -127,6 +128,7 @@ Menu.setApplicationMenu(Menu.buildFromTemplate([ app.whenReady().then(async () => { await ensureRuntimeDirectory(); + setCacheDir(getRuntimeDirectory()); const persistedDestination = await loadPersistedDestination(); config = buildConfig(persistedDestination); createWindow(); @@ -224,9 +226,11 @@ ipcMain.handle('update-destination', async (_event, payload) => { config = buildConfig(destination); await persistDestination(config.destination); + await clearCache(); return { destination: config.destination }; }); + ipcMain.handle('open-unrouted-folder', async () => { const unroutedDir = await resolveUnroutedDir(); const openResult = await shell.openPath(unroutedDir); diff --git a/renderer/renderer.js b/renderer/renderer.js index 6041106..6b639d5 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -35,19 +35,34 @@ function hideProgress() { let _indexFilesFound = 0; function updateProgress({ phase, current, total, file, scanned, folder }) { - if (phase === 'scan') { + if (phase === 'index-cache') { + // folderIndex dalla cache — avvia scansione veloce dei file + progressBar.max = total || 1; + progressBar.value = 0; + progressLabel.textContent = 'Struttura dalla cache — ricerca file CAD in corso...'; + return; + } else if (phase === 'scan') { _indexFilesFound = 0; progressLabel.textContent = 'Scansione file sorgente...'; progressBar.removeAttribute('value'); } else if (phase === 'index-dest') { + // scansione completa (nessuna cache) progressBar.max = total; progressBar.value = current; const filesInfo = _indexFilesFound > 0 ? ` — ${_indexFilesFound} file CAD trovati` : ''; progressLabel.textContent = `Analisi destinazione: ${current} / ${total}${folder ? ` — ${folder}` : ''}${filesInfo}`; } else if (phase === 'index-dup') { - _indexFilesFound = scanned; - const dirInfo = progressBar.max > 0 ? `${progressBar.value} / ${progressBar.max} — ` : ''; - progressLabel.textContent = `Analisi destinazione: ${dirInfo}${scanned} file CAD trovati`; + if (current !== undefined && total !== undefined) { + // scansione veloce (cache attiva): current/total = cartelle processate + progressBar.max = total; + progressBar.value = current; + progressLabel.textContent = `Ricerca file CAD: ${current} / ${total} cartelle`; + } else { + // scansione completa: scanned = contatore file trovati + _indexFilesFound = scanned; + const dirInfo = progressBar.max > 0 ? `${progressBar.value} / ${progressBar.max} — ` : ''; + progressLabel.textContent = `Analisi destinazione: ${dirInfo}${scanned} file CAD trovati`; + } } else if (phase === 'copy') { progressBar.max = total; progressBar.value = current; @@ -349,6 +364,7 @@ clearPreviewBtn.addEventListener('click', async () => { } }); + closePreviewBtn.addEventListener('click', hidePreview); previewOverlay.addEventListener('click', (event) => { if (event.target === previewOverlay) { diff --git a/services/destinationScanner.js b/services/destinationScanner.js index 0cea3de..a31ef51 100644 --- a/services/destinationScanner.js +++ b/services/destinationScanner.js @@ -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 (per il routing) - * - cadKeyIndex: Set (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 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 }; diff --git a/services/router.js b/services/router.js index 502ce21..8e9f013 100644 --- a/services/router.js +++ b/services/router.js @@ -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) {