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:
4
main.js
4
main.js
@@ -4,6 +4,7 @@ const fs = require('fs-extra');
|
|||||||
|
|
||||||
const { processFolder } = require('./services/folderProcessor');
|
const { processFolder } = require('./services/folderProcessor');
|
||||||
const { processZip } = require('./services/zipProcessor');
|
const { processZip } = require('./services/zipProcessor');
|
||||||
|
const { setCacheDir, clearCache } = require('./services/destinationScanner');
|
||||||
const {
|
const {
|
||||||
resolveUnroutedDir,
|
resolveUnroutedDir,
|
||||||
listUnroutedFiles,
|
listUnroutedFiles,
|
||||||
@@ -127,6 +128,7 @@ Menu.setApplicationMenu(Menu.buildFromTemplate([
|
|||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
await ensureRuntimeDirectory();
|
await ensureRuntimeDirectory();
|
||||||
|
setCacheDir(getRuntimeDirectory());
|
||||||
const persistedDestination = await loadPersistedDestination();
|
const persistedDestination = await loadPersistedDestination();
|
||||||
config = buildConfig(persistedDestination);
|
config = buildConfig(persistedDestination);
|
||||||
createWindow();
|
createWindow();
|
||||||
@@ -224,9 +226,11 @@ ipcMain.handle('update-destination', async (_event, payload) => {
|
|||||||
|
|
||||||
config = buildConfig(destination);
|
config = buildConfig(destination);
|
||||||
await persistDestination(config.destination);
|
await persistDestination(config.destination);
|
||||||
|
await clearCache();
|
||||||
return { destination: config.destination };
|
return { destination: config.destination };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('open-unrouted-folder', async () => {
|
ipcMain.handle('open-unrouted-folder', async () => {
|
||||||
const unroutedDir = await resolveUnroutedDir();
|
const unroutedDir = await resolveUnroutedDir();
|
||||||
const openResult = await shell.openPath(unroutedDir);
|
const openResult = await shell.openPath(unroutedDir);
|
||||||
|
|||||||
@@ -35,19 +35,34 @@ function hideProgress() {
|
|||||||
let _indexFilesFound = 0;
|
let _indexFilesFound = 0;
|
||||||
|
|
||||||
function updateProgress({ phase, current, total, file, scanned, folder }) {
|
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;
|
_indexFilesFound = 0;
|
||||||
progressLabel.textContent = 'Scansione file sorgente...';
|
progressLabel.textContent = 'Scansione file sorgente...';
|
||||||
progressBar.removeAttribute('value');
|
progressBar.removeAttribute('value');
|
||||||
} else if (phase === 'index-dest') {
|
} else if (phase === 'index-dest') {
|
||||||
|
// scansione completa (nessuna cache)
|
||||||
progressBar.max = total;
|
progressBar.max = total;
|
||||||
progressBar.value = current;
|
progressBar.value = current;
|
||||||
const filesInfo = _indexFilesFound > 0 ? ` — ${_indexFilesFound} file CAD trovati` : '';
|
const filesInfo = _indexFilesFound > 0 ? ` — ${_indexFilesFound} file CAD trovati` : '';
|
||||||
progressLabel.textContent = `Analisi destinazione: ${current} / ${total}${folder ? ` — ${folder}` : ''}${filesInfo}`;
|
progressLabel.textContent = `Analisi destinazione: ${current} / ${total}${folder ? ` — ${folder}` : ''}${filesInfo}`;
|
||||||
} else if (phase === 'index-dup') {
|
} else if (phase === 'index-dup') {
|
||||||
_indexFilesFound = scanned;
|
if (current !== undefined && total !== undefined) {
|
||||||
const dirInfo = progressBar.max > 0 ? `${progressBar.value} / ${progressBar.max} — ` : '';
|
// scansione veloce (cache attiva): current/total = cartelle processate
|
||||||
progressLabel.textContent = `Analisi destinazione: ${dirInfo}${scanned} file CAD trovati`;
|
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') {
|
} else if (phase === 'copy') {
|
||||||
progressBar.max = total;
|
progressBar.max = total;
|
||||||
progressBar.value = current;
|
progressBar.value = current;
|
||||||
@@ -349,6 +364,7 @@ clearPreviewBtn.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
closePreviewBtn.addEventListener('click', hidePreview);
|
closePreviewBtn.addEventListener('click', hidePreview);
|
||||||
previewOverlay.addEventListener('click', (event) => {
|
previewOverlay.addEventListener('click', (event) => {
|
||||||
if (event.target === previewOverlay) {
|
if (event.target === previewOverlay) {
|
||||||
|
|||||||
@@ -4,21 +4,64 @@ const { getCadInfo } = require('./router');
|
|||||||
|
|
||||||
const CODE_FOLDER_REGEX = /^\d{3}$/;
|
const CODE_FOLDER_REGEX = /^\d{3}$/;
|
||||||
const MAX_SCAN_DEPTH = 6;
|
const MAX_SCAN_DEPTH = 6;
|
||||||
|
const CACHE_FILENAME = 'dest-index.cache.json';
|
||||||
|
|
||||||
|
let _cacheDir = null;
|
||||||
|
|
||||||
|
function setCacheDir(dir) {
|
||||||
|
_cacheDir = dir;
|
||||||
|
}
|
||||||
|
|
||||||
function toCadKey(cadInfo) {
|
function toCadKey(cadInfo) {
|
||||||
return String(cadInfo?.key || '').toLowerCase();
|
return String(cadInfo?.key || '').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getCacheFilePath() {
|
||||||
* Scansiona la cartella di destinazione in un unico passaggio parallelo e
|
return _cacheDir ? path.join(_cacheDir, CACHE_FILENAME) : null;
|
||||||
* costruisce contemporaneamente:
|
}
|
||||||
* - folderIndex: Map<group3digits, fullPath[]> (per il routing)
|
|
||||||
* - cadKeyIndex: Set<cadKey> (per il rilevamento duplicati)
|
// ── Cache su disco (solo folderIndex — struttura cartelle) ─────────────────
|
||||||
*
|
|
||||||
* Legge prima le sottocartelle di primo livello (1 sola readdir) per ottenere
|
async function tryLoadFolderIndexCache(destinationRoot) {
|
||||||
* il totale e mostrare una barra determinata durante la scansione.
|
const cacheFile = getCacheFilePath();
|
||||||
*/
|
if (!cacheFile) return null;
|
||||||
async function buildDestinationIndexes(destinationRoot, onProgress) {
|
|
||||||
|
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 folderIndex = new Map();
|
||||||
const cadKeyIndex = new Set();
|
const cadKeyIndex = new Set();
|
||||||
|
|
||||||
@@ -26,7 +69,6 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
|
|||||||
return { folderIndex, cadKeyIndex };
|
return { folderIndex, cadKeyIndex };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lettura di primo livello: 1 sola chiamata per avere il totale
|
|
||||||
let topEntries;
|
let topEntries;
|
||||||
try {
|
try {
|
||||||
topEntries = await fs.readdir(destinationRoot, { withFileTypes: true });
|
topEntries = await fs.readdir(destinationRoot, { withFileTypes: true });
|
||||||
@@ -42,7 +84,6 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
|
|||||||
let completed = 0;
|
let completed = 0;
|
||||||
let scannedFiles = 0;
|
let scannedFiles = 0;
|
||||||
|
|
||||||
// Registra i file CAD al primo livello (non cartelle)
|
|
||||||
for (const entry of topEntries) {
|
for (const entry of topEntries) {
|
||||||
if (!entry.isFile()) continue;
|
if (!entry.isFile()) continue;
|
||||||
const cadInfo = getCadInfo(entry.name);
|
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) {
|
async function walkSubtree(dirPath, depth) {
|
||||||
if (depth > MAX_SCAN_DEPTH) {
|
if (depth > MAX_SCAN_DEPTH) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
@@ -90,7 +128,6 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
|
|||||||
await Promise.all(subdirPromises);
|
await Promise.all(subdirPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processa tutte le cartelle di primo livello in parallelo
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
topDirs.map(async ({ name, fullPath }) => {
|
topDirs.map(async ({ name, fullPath }) => {
|
||||||
onProgress?.({ phase: 'index-dest', current: completed, total, folder: name });
|
onProgress?.({ phase: 'index-dest', current: completed, total, folder: name });
|
||||||
@@ -111,4 +148,117 @@ async function buildDestinationIndexes(destinationRoot, onProgress) {
|
|||||||
return { folderIndex, cadKeyIndex };
|
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 };
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ function getCadInfo(filename) {
|
|||||||
const version = match[3] || '';
|
const version = match[3] || '';
|
||||||
const digits = rawCode.replace(/\D/g, '');
|
const digits = rawCode.replace(/\D/g, '');
|
||||||
const routingGroup = digits.length >= 5 ? digits.slice(2, 5) : null;
|
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 {
|
return {
|
||||||
code: rawCode,
|
code: rawCode,
|
||||||
@@ -24,6 +27,7 @@ function getCadInfo(filename) {
|
|||||||
version,
|
version,
|
||||||
key: `${rawCode}.${type}`,
|
key: `${rawCode}.${type}`,
|
||||||
routingGroup,
|
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) {
|
function findDestination(filename, config, destinationIndex) {
|
||||||
|
|||||||
Reference in New Issue
Block a user