Compare commits

...

7 Commits

Author SHA1 Message Date
9aec9d3a50 refactor: rimuove cache destinazione e forza rescansione completa
- elimina cache su disco in destination scanner
- rimuove integrazione cache da main process
- allinea messaggi progresso UI alla scansione completa
- aggiorna README con comportamento senza cache
2026-03-16 16:27:55 +01:00
65b1777a60 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
2026-03-16 16:17:32 +01:00
a49d338741 perf(scanner): ottimizza scansione destinazione con pass unico e barra determinata
Sostituisce le due scansioni sequenziali (cartelle + file CAD) con un
unico passaggio parallelo in services/destinationScanner.js. La lettura
di primo livello fornisce il totale delle sezioni, rendendo la barra
di progresso determinata (N / totale) durante l'analisi della destinazione.
Il label mostra contemporaneamente sezioni completate e file CAD trovati
2026-03-16 15:16:27 +01:00
b0bcea2f10 feat(ui): aggiunge barra di progresso durante lo smistamento
Mostra una progress bar con messaggio contestuale nelle 4 fasi:
scansione sorgente, analisi cartella destinazione (con contatore
cartelle), ricerca duplicati (con contatore file CAD), smistamento
(N / totale). La barra è indeterminata nelle prime tre fasi e
determinata durante la copia. Il canale IPC progress viene
aperto all'inizio e chiuso al termine dell'operazione
2026-03-16 14:54:23 +01:00
57f8b230b2 docs: aggiorna documentazione cartelle speciali con percorsi corretti
Aggiunge sezione __SALTATI nella guida utente e nella sidebar.
Corregge i percorsi di __NON_SMISTATI e __DUPLICATI (Windows: C:\cadroute\,
Linux: ~/.cadroute/) e aggiorna i riferimenti da duplicati/ a __DUPLICATI
in tutta la documentazione. Aggiorna README con i percorsi corretti
2026-03-16 14:33:19 +01:00
fea0699ff1 fix(router): corregge estensione .dwr in .drw (formato Creo corretto)
Creo utilizza .drw per i disegni 2D, non .dwr. Rimossa la normalizzazione
errata drw→dwr nel router, aggiornata la regex, CAD_EXTENSIONS e la
documentazione utente
2026-03-16 13:56:10 +01:00
82becb7569 Aggiunge LICENSE e README.md 2026-03-16 13:33:10 +01:00
13 changed files with 378 additions and 38 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Davide Grilli
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

60
README.md Normal file
View File

@@ -0,0 +1,60 @@
# CadRoute
Smistatore automatico di file CAD (Creo) per Windows e Linux.
Analizza una cartella o un archivio `.zip`, riconosce i file CAD (`.prt`, `.asm`, `.drw`) e li copia automaticamente nella sottocartella di destinazione corretta, basandosi sulla struttura numerica del nome file.
## Funzionalità
- Smistamento da cartella o archivio ZIP (con drag & drop)
- Riscansione completa della destinazione a ogni processo (senza cache)
- Routing automatico basato sul gruppo di cifre nel nome file
- Gestione versioni: mantiene solo la versione più alta
- Cartelle speciali: `__NON_SMISTATI`, `__DUPLICATI`, `__SALTATI`
- Anteprima e pulizia delle cartelle speciali
- Guida utente integrata (Help → Documentazione)
- Destinazione persistente tra le sessioni
## Avvio in sviluppo
```bash
npm install
npm run dev
```
## Build
### Linux (Docker)
```bash
cd contrib/linux
./build.sh
```
### Windows (Docker + Wine)
```bash
cd contrib/windows
./build.sh
```
Gli artefatti vengono generati in `dist/`.
## Struttura cartelle speciali
| Cartella | Contenuto |
|----------|-----------|
| `__NON_SMISTATI` | File CAD senza destinazione valida |
| `__DUPLICATI` | File CAD già presenti in destinazione |
| `__SALTATI` | File non CAD (PDF, immagini, ecc.) |
- **Windows:** `C:\cadroute\<cartella>\`
- **Linux:** `~/.cadroute/<cartella>/`
## Autore
Davide Grilli — Cevolani Italia s.r.l.
## Licenza
MIT

21
main.js
View File

@@ -14,7 +14,7 @@ const {
clearSkippedFiles,
} = require('./services/unrouted');
const CAD_EXTENSIONS = ['prt', 'asm', 'dwr'];
const CAD_EXTENSIONS = ['prt', 'asm', 'drw'];
const DEFAULT_DESTINATION = 'X:\\';
const SETTINGS_FILENAME = 'cad-router-settings.json';
const LINUX_RUNTIME_DIR = '.cadroute';
@@ -142,7 +142,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 +151,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 +166,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 +182,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 };
}
@@ -223,6 +227,7 @@ ipcMain.handle('update-destination', async (_event, payload) => {
return { destination: config.destination };
});
ipcMain.handle('open-unrouted-folder', async () => {
const unroutedDir = await resolveUnroutedDir();
const openResult = await shell.openPath(unroutedDir);

View File

@@ -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'),
});

View File

@@ -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>
@@ -55,14 +56,14 @@
<td>File di assemblaggio Creo</td>
</tr>
<tr>
<td><span class="badge dwr">.dwr</span></td>
<td><span class="badge dwr">.drw</span></td>
<td>Drawing</td>
<td>File di disegno Creo</td>
</tr>
<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>

View File

@@ -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">

View File

@@ -15,10 +15,52 @@ 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 === 'scan') {
_indexFilesFound = 0;
progressLabel.textContent = 'Scansione file sorgente...';
progressBar.removeAttribute('value');
} else if (phase === 'index-dest') {
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) {
progressBar.max = total;
progressBar.value = current;
progressLabel.textContent = `Analisi destinazione: ${current} / ${total} cartelle`;
} else {
_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 +143,8 @@ async function handleAction(actionName, actionFn) {
isProcessing = true;
setLoading(true);
showProgress();
window.api.onProgress(updateProgress);
output.textContent = `${actionName} in corso...`;
try {
@@ -115,6 +159,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 +355,7 @@ clearPreviewBtn.addEventListener('click', async () => {
}
});
closePreviewBtn.addEventListener('click', hidePreview);
previewOverlay.addEventListener('click', (event) => {
if (event.target === previewOverlay) {

View File

@@ -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);

View File

@@ -0,0 +1,99 @@
const fs = require('fs-extra');
const path = require('path');
const { getCadInfo } = require('./router');
const CODE_FOLDER_REGEX = /^\d{3}$/;
const MAX_SCAN_DEPTH = 6;
function toCadKey(cadInfo) {
return String(cadInfo?.key || '').toLowerCase();
}
async function buildDestinationIndexes(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 };
}
module.exports = { buildDestinationIndexes, toCadKey };

View File

@@ -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));
}
}

View File

@@ -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) {

View File

@@ -1,13 +1,12 @@
const path = require('path');
function normalizeCadType(ext) {
const lowerExt = String(ext || '').toLowerCase();
return lowerExt === 'drw' ? 'dwr' : lowerExt;
return String(ext || '').toLowerCase();
}
function getCadInfo(filename) {
const baseName = path.basename(filename);
const match = baseName.match(/^(.*)\.(prt|asm|drw|dwr)(?:\.([^.]+))?$/i);
const match = baseName.match(/^(.*)\.(prt|asm|drw)(?:\.([^.]+))?$/i);
if (!match) {
return null;
}
@@ -17,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,
@@ -25,6 +27,7 @@ function getCadInfo(filename) {
version,
key: `${rawCode}.${type}`,
routingGroup,
subfolder,
};
}
@@ -58,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) {

View File

@@ -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) {