Compare commits
4 Commits
fea0699ff1
...
65b1777a60
| Author | SHA1 | Date | |
|---|---|---|---|
| 65b1777a60 | |||
| a49d338741 | |||
| b0bcea2f10 | |||
| 57f8b230b2 |
@@ -47,8 +47,8 @@ Gli artefatti vengono generati in `dist/`.
|
||||
| `__DUPLICATI` | File CAD già presenti in destinazione |
|
||||
| `__SALTATI` | File non CAD (PDF, immagini, ecc.) |
|
||||
|
||||
- **Windows:** `C:\cadroute\<cartella>\`
|
||||
- **Linux:** `~/.cadroute/<cartella>/`
|
||||
- **Windows:** `%APPDATA%\CadRoute\<cartella>\`
|
||||
|
||||
## Autore
|
||||
|
||||
|
||||
22
main.js
22
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();
|
||||
@@ -142,7 +144,7 @@ app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
ipcMain.handle('select-folder', async () => {
|
||||
ipcMain.handle('select-folder', async (event) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
@@ -151,11 +153,12 @@ ipcMain.handle('select-folder', async () => {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
const routingResult = await processFolder(result.filePaths[0], config);
|
||||
const onProgress = (data) => event.sender.send('progress', data);
|
||||
const routingResult = await processFolder(result.filePaths[0], config, onProgress);
|
||||
return { canceled: false, ...routingResult };
|
||||
});
|
||||
|
||||
ipcMain.handle('select-zip', async () => {
|
||||
ipcMain.handle('select-zip', async (event) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Zip', extensions: ['zip'] }],
|
||||
@@ -165,11 +168,12 @@ ipcMain.handle('select-zip', async () => {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
const routingResult = await processZip(result.filePaths[0], config);
|
||||
const onProgress = (data) => event.sender.send('progress', data);
|
||||
const routingResult = await processZip(result.filePaths[0], config, onProgress);
|
||||
return { canceled: false, ...routingResult };
|
||||
});
|
||||
|
||||
ipcMain.handle('process-dropped-path', async (_event, payload) => {
|
||||
ipcMain.handle('process-dropped-path', async (event, payload) => {
|
||||
const droppedPath = String(payload?.path || '').trim();
|
||||
if (!droppedPath) {
|
||||
throw new Error('Percorso non valido');
|
||||
@@ -180,13 +184,15 @@ ipcMain.handle('process-dropped-path', async (_event, payload) => {
|
||||
throw new Error('Percorso non trovato');
|
||||
}
|
||||
|
||||
const onProgress = (data) => event.sender.send('progress', data);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const routingResult = await processFolder(droppedPath, config);
|
||||
const routingResult = await processFolder(droppedPath, config, onProgress);
|
||||
return { canceled: false, sourceType: 'folder', ...routingResult };
|
||||
}
|
||||
|
||||
if (stats.isFile() && path.extname(droppedPath).toLowerCase() === '.zip') {
|
||||
const routingResult = await processZip(droppedPath, config);
|
||||
const routingResult = await processZip(droppedPath, config, onProgress);
|
||||
return { canceled: false, sourceType: 'zip', ...routingResult };
|
||||
}
|
||||
|
||||
@@ -220,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);
|
||||
|
||||
@@ -15,4 +15,6 @@ contextBridge.exposeInMainWorld('api', {
|
||||
clearUnroutedFiles: () => ipcRenderer.invoke('clear-unrouted-files'),
|
||||
clearDuplicatesFiles: () => ipcRenderer.invoke('clear-duplicates-files'),
|
||||
clearSkippedFiles: () => ipcRenderer.invoke('clear-skipped-files'),
|
||||
onProgress: (cb) => ipcRenderer.on('progress', (_e, data) => cb(data)),
|
||||
offProgress: () => ipcRenderer.removeAllListeners('progress'),
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<a href="#nomi">Convenzione nomi file</a>
|
||||
<a href="#nonsmistati">File non smistati</a>
|
||||
<a href="#duplicati">File duplicati</a>
|
||||
<a href="#saltati">File saltati</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
@@ -62,7 +63,7 @@
|
||||
<tr>
|
||||
<td><span class="badge skip">altri</span></td>
|
||||
<td>—</td>
|
||||
<td>Ignorati (PDF, immagini, documenti, ecc.)</td>
|
||||
<td>Copiati in <code>__SALTATI</code> (PDF, immagini, documenti, ecc.)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -100,7 +101,7 @@
|
||||
<ol class="steps">
|
||||
<li><span>Clicca <strong>Process Folder</strong> per selezionare una cartella, oppure <strong>Process ZIP</strong> per selezionare un archivio. In alternativa, trascina la cartella o il file <code>.zip</code> nell'area tratteggiata.</span></li>
|
||||
<li><span>L'app scansiona tutti i file e identifica automaticamente quelli CAD.</span></li>
|
||||
<li><span>I file CAD vengono copiati nella sottocartella di destinazione corrispondente. I file non CAD vengono saltati.</span></li>
|
||||
<li><span>I file CAD vengono copiati nella sottocartella di destinazione corrispondente. I file non CAD vengono copiati in <code>__SALTATI</code>.</span></li>
|
||||
<li><span>Al termine vengono mostrate le statistiche nell'area di output.</span></li>
|
||||
</ol>
|
||||
|
||||
@@ -127,7 +128,7 @@
|
||||
</div>
|
||||
<div class="stat-item muted">
|
||||
<div class="stat-label">Saltati</div>
|
||||
<div class="stat-desc">File ignorati perché non CAD (PDF, immagini, ecc.).</div>
|
||||
<div class="stat-desc">File non CAD copiati in <code>__SALTATI</code> (PDF, immagini, ecc.).</div>
|
||||
</div>
|
||||
<div class="stat-item warn">
|
||||
<div class="stat-label">Non smistati</div>
|
||||
@@ -172,7 +173,7 @@ Esempi:
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File già presente in destinazione</td>
|
||||
<td>Copiato in <code>duplicati/</code> per revisione</td>
|
||||
<td>Copiato in <code>__DUPLICATI</code> per revisione</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Versione inferiore presente nella sorgente</td>
|
||||
@@ -210,8 +211,8 @@ Esempi:
|
||||
<h3>Dove si trovano</h3>
|
||||
<p>I file non smistati vengono copiati nella cartella <code>__NON_SMISTATI</code>:</p>
|
||||
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
|
||||
<li><strong>Windows:</strong> <code>C:\cadroute\__NON_SMISTATI\</code></li>
|
||||
<li><strong>Linux:</strong> <code>~/.cadroute/__NON_SMISTATI/</code></li>
|
||||
<li><strong>Windows:</strong> <code>%APPDATA%\CadRoute\__NON_SMISTATI\</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Visualizzare e pulire</h3>
|
||||
@@ -231,16 +232,16 @@ Esempi:
|
||||
<h2>File duplicati</h2>
|
||||
<p>Un file CAD viene classificato come <strong>duplicato</strong> quando la sua chiave (<code>CODICE.tipo</code>, senza versione) è già presente nella cartella di destinazione al momento dello smistamento.</p>
|
||||
|
||||
<p>I duplicati non sovrascrivono i file già presenti in destinazione: vengono messi da parte nella cartella <code>duplicati/</code> per permettere una revisione manuale.</p>
|
||||
<p>I duplicati non sovrascrivono i file già presenti in destinazione: vengono messi da parte nella cartella <code>__DUPLICATI</code> per permettere una revisione manuale.</p>
|
||||
|
||||
<h3>Dove si trovano</h3>
|
||||
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
|
||||
<li><strong>Linux:</strong> <code>~/.cadroute/duplicati/</code></li>
|
||||
<li><strong>Windows:</strong> <code>%APPDATA%\CadRoute\duplicati\</code></li>
|
||||
<li><strong>Windows:</strong> <code>C:\cadroute\__DUPLICATI\</code></li>
|
||||
<li><strong>Linux:</strong> <code>~/.cadroute/__DUPLICATI/</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Gestione versioni nei duplicati</h3>
|
||||
<p>Anche all'interno della cartella <code>duplicati/</code> viene applicata la logica delle versioni: se arriva una versione più alta di un file già presente nei duplicati, la versione vecchia viene eliminata e sostituita con quella nuova.</p>
|
||||
<p>Anche all'interno della cartella <code>__DUPLICATI</code> viene applicata la logica delle versioni: se arriva una versione più alta di un file già presente nei duplicati, la versione vecchia viene eliminata e sostituita con quella nuova.</p>
|
||||
|
||||
<h3>Visualizzare e pulire</h3>
|
||||
<ol class="steps">
|
||||
@@ -254,6 +255,31 @@ Esempi:
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── SALTATI ────────────────────────────── -->
|
||||
<section id="saltati">
|
||||
<h2>File saltati</h2>
|
||||
<p>Un file viene classificato come <strong>saltato</strong> quando non è riconosciuto come file CAD, cioè non ha estensione <code>.prt</code>, <code>.asm</code> o <code>.drw</code>. Rientrano in questa categoria PDF, immagini, documenti Word e qualsiasi altro formato non CAD.</p>
|
||||
|
||||
<p>I file saltati non vengono ignorati completamente: vengono copiati nella cartella <code>__SALTATI</code> per permettere una verifica manuale.</p>
|
||||
|
||||
<h3>Dove si trovano</h3>
|
||||
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
|
||||
<li><strong>Windows:</strong> <code>C:\cadroute\__SALTATI\</code></li>
|
||||
<li><strong>Linux:</strong> <code>~/.cadroute/__SALTATI/</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Visualizzare e pulire</h3>
|
||||
<ol class="steps">
|
||||
<li><span>Clicca <strong>Anteprima saltati</strong> per aprire la lista dei file presenti.</span></li>
|
||||
<li><span>Verifica che nessun file CAD sia finito qui per errore di denominazione.</span></li>
|
||||
<li><span>Se vuoi svuotare la cartella, clicca <strong>Pulisci cartella</strong> (rosso) e conferma.</span></li>
|
||||
</ol>
|
||||
|
||||
<div class="callout info">
|
||||
<strong>Nota:</strong> la cartella <code>__SALTATI</code> è utile per individuare file CAD rinominati erroneamente o con estensione mancante.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -254,6 +254,53 @@
|
||||
overflow: auto;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: 18px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
progress {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
background: var(--accent);
|
||||
border-radius: 4px;
|
||||
transition: width 80ms ease;
|
||||
}
|
||||
|
||||
progress:indeterminate {
|
||||
background: linear-gradient(90deg, var(--border) 25%, var(--accent) 50%, var(--border) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: progress-indeterminate 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-indeterminate {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -270,6 +317,11 @@
|
||||
Trascina qui una cartella o un file .zip
|
||||
</section>
|
||||
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-label" id="progressLabel">In corso...</div>
|
||||
<progress id="progressBar"></progress>
|
||||
</div>
|
||||
|
||||
<section class="rules">
|
||||
<h2>Destinazione file CAD</h2>
|
||||
<div class="rule-row single-row">
|
||||
|
||||
@@ -15,10 +15,61 @@ const clearPreviewBtn = document.getElementById('clearPreviewBtn');
|
||||
const previewTitle = document.getElementById('previewTitle');
|
||||
const previewMeta = document.getElementById('previewMeta');
|
||||
const previewList = document.getElementById('previewList');
|
||||
const progressContainer = document.getElementById('progressContainer');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressLabel = document.getElementById('progressLabel');
|
||||
const defaultDropZoneText = 'Trascina qui una cartella o un file .zip';
|
||||
let isProcessing = false;
|
||||
let currentPreviewKind = null;
|
||||
|
||||
function showProgress() {
|
||||
progressContainer.style.display = 'block';
|
||||
progressBar.removeAttribute('value');
|
||||
progressLabel.textContent = 'In corso...';
|
||||
}
|
||||
|
||||
function hideProgress() {
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
let _indexFilesFound = 0;
|
||||
|
||||
function updateProgress({ phase, current, total, file, scanned, folder }) {
|
||||
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') {
|
||||
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;
|
||||
progressLabel.textContent = `Smistamento: ${current} / ${total}${file ? ` — ${file}` : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
folderBtn.disabled = isLoading;
|
||||
zipBtn.disabled = isLoading;
|
||||
@@ -101,6 +152,8 @@ async function handleAction(actionName, actionFn) {
|
||||
|
||||
isProcessing = true;
|
||||
setLoading(true);
|
||||
showProgress();
|
||||
window.api.onProgress(updateProgress);
|
||||
output.textContent = `${actionName} in corso...`;
|
||||
|
||||
try {
|
||||
@@ -115,6 +168,8 @@ async function handleAction(actionName, actionFn) {
|
||||
} catch (error) {
|
||||
output.textContent = `${actionName} fallito:\n${error.message}`;
|
||||
} finally {
|
||||
window.api.offProgress();
|
||||
hideProgress();
|
||||
setLoading(false);
|
||||
isProcessing = false;
|
||||
}
|
||||
@@ -309,6 +364,7 @@ clearPreviewBtn.addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
closePreviewBtn.addEventListener('click', hidePreview);
|
||||
previewOverlay.addEventListener('click', (event) => {
|
||||
if (event.target === previewOverlay) {
|
||||
|
||||
@@ -4,13 +4,15 @@ const path = require('path');
|
||||
const CODE_FOLDER_REGEX = /^\d{3}$/;
|
||||
const MAX_SCAN_DEPTH = 6;
|
||||
|
||||
async function buildDestinationIndex(destinationRoot) {
|
||||
async function buildDestinationIndex(destinationRoot, onProgress) {
|
||||
const index = new Map();
|
||||
|
||||
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
|
||||
return index;
|
||||
}
|
||||
|
||||
let scanned = 0;
|
||||
|
||||
async function walk(currentDir, depth) {
|
||||
if (depth > MAX_SCAN_DEPTH) {
|
||||
return;
|
||||
@@ -29,6 +31,9 @@ async function buildDestinationIndex(destinationRoot) {
|
||||
}
|
||||
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
scanned += 1;
|
||||
onProgress?.({ phase: 'index-dest', scanned, folder: entry.name });
|
||||
|
||||
if (CODE_FOLDER_REGEX.test(entry.name)) {
|
||||
const rows = index.get(entry.name) || [];
|
||||
rows.push(fullPath);
|
||||
|
||||
264
services/destinationScanner.js
Normal file
264
services/destinationScanner.js
Normal file
@@ -0,0 +1,264 @@
|
||||
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<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 };
|
||||
@@ -8,13 +8,15 @@ function toCadKey(cadInfo) {
|
||||
return String(cadInfo?.key || '').toLowerCase();
|
||||
}
|
||||
|
||||
async function buildExistingCadKeyIndex(destinationRoot) {
|
||||
async function buildExistingCadKeyIndex(destinationRoot, onProgress) {
|
||||
const keys = new Set();
|
||||
|
||||
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
let scanned = 0;
|
||||
|
||||
async function walk(currentDir, depth) {
|
||||
if (depth > MAX_SCAN_DEPTH) {
|
||||
return;
|
||||
@@ -44,6 +46,8 @@ async function buildExistingCadKeyIndex(destinationRoot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
scanned += 1;
|
||||
onProgress?.({ phase: 'index-dup', scanned, file: entry.name });
|
||||
keys.add(toCadKey(cadInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { getDestinationDecision, getCadInfo } = require('./router');
|
||||
const { buildDestinationIndex } = require('./destinationIndex');
|
||||
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
|
||||
const { buildDestinationIndexes, toCadKey } = require('./destinationScanner');
|
||||
const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted');
|
||||
|
||||
function parseNumericVersion(version) {
|
||||
@@ -66,10 +65,11 @@ async function collectFilesRecursively(rootDir) {
|
||||
return files;
|
||||
}
|
||||
|
||||
async function processFolder(folder, config) {
|
||||
async function processFolder(folder, config, onProgress) {
|
||||
onProgress?.({ phase: 'scan' });
|
||||
const files = await collectFilesRecursively(folder);
|
||||
const destinationIndex = await buildDestinationIndex(config?.destination);
|
||||
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
|
||||
|
||||
const { folderIndex: destinationIndex, cadKeyIndex: existingCadKeys } = await buildDestinationIndexes(config?.destination, onProgress);
|
||||
const sourceMaxVersions = buildHighestNumericVersionByCadKey(files);
|
||||
const result = {
|
||||
scanned: 0,
|
||||
@@ -80,9 +80,13 @@ async function processFolder(folder, config) {
|
||||
details: [],
|
||||
};
|
||||
|
||||
for (const { fullPath, fileName } of files) {
|
||||
const total = files.length;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const { fullPath, fileName } = files[i];
|
||||
const file = fileName;
|
||||
result.scanned += 1;
|
||||
onProgress?.({ phase: 'copy', current: i + 1, total, file });
|
||||
|
||||
const cadInfo = getCadInfo(file);
|
||||
if (!cadInfo) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -3,8 +3,7 @@ const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { pipeline } = require('stream/promises');
|
||||
const { getDestinationDecision, getCadInfo } = require('./router');
|
||||
const { buildDestinationIndex } = require('./destinationIndex');
|
||||
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
|
||||
const { buildDestinationIndexes, toCadKey } = require('./destinationScanner');
|
||||
const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted');
|
||||
|
||||
function parseNumericVersion(version) {
|
||||
@@ -27,7 +26,7 @@ async function buildZipHighestNumericVersionByCadKey(zipPath) {
|
||||
try {
|
||||
directory = await unzipper.Open.file(zipPath);
|
||||
} catch {
|
||||
return index;
|
||||
return { index, total: 0 };
|
||||
}
|
||||
|
||||
for (const row of directory.files || []) {
|
||||
@@ -53,14 +52,16 @@ async function buildZipHighestNumericVersionByCadKey(zipPath) {
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
return { index, total: (directory.files || []).filter((f) => f.type === 'File').length };
|
||||
}
|
||||
|
||||
async function processZip(zipPath, config) {
|
||||
async function processZip(zipPath, config, onProgress) {
|
||||
onProgress?.({ phase: 'scan' });
|
||||
const { index: sourceMaxVersions, total } = await buildZipHighestNumericVersionByCadKey(zipPath);
|
||||
|
||||
const { folderIndex: destinationIndex, cadKeyIndex: existingCadKeys } = await buildDestinationIndexes(config?.destination, onProgress);
|
||||
|
||||
const stream = fs.createReadStream(zipPath).pipe(unzipper.Parse({ forceStream: true }));
|
||||
const destinationIndex = await buildDestinationIndex(config?.destination);
|
||||
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
|
||||
const sourceMaxVersions = await buildZipHighestNumericVersionByCadKey(zipPath);
|
||||
const result = {
|
||||
scanned: 0,
|
||||
copied: 0,
|
||||
@@ -70,6 +71,8 @@ async function processZip(zipPath, config) {
|
||||
details: [],
|
||||
};
|
||||
|
||||
let current = 0;
|
||||
|
||||
for await (const entry of stream) {
|
||||
if (entry.type !== 'File') {
|
||||
entry.autodrain();
|
||||
@@ -78,7 +81,9 @@ async function processZip(zipPath, config) {
|
||||
|
||||
const file = entry.path;
|
||||
const baseName = path.basename(file);
|
||||
current += 1;
|
||||
result.scanned += 1;
|
||||
onProgress?.({ phase: 'copy', current, total, file: baseName });
|
||||
|
||||
const cadInfo = getCadInfo(baseName);
|
||||
if (!cadInfo) {
|
||||
|
||||
Reference in New Issue
Block a user