Compare commits

..

10 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
5e5dadba2c feat(ui): aggiunge menu Informazioni con dialog about (versione, autore, azienda) 2026-03-16 12:28:52 +01:00
6438d1a51c feat(ui): aumenta dimensione finestra e scala UI (font, pulsanti, riquadri) 2026-03-16 12:19:01 +01:00
3fa7758fbe feat: smistamento file non-CAD, rinomina cartelle speciali, guida utente
- Aggiunge cartella __SALTATI: i file non-CAD vengono ora copiati in
  __SALTATI invece di essere ignorati (folderProcessor, zipProcessor)
- Rinomina cartella duplicati in __DUPLICATI per coerenza con le altre
  cartelle speciali (__NON_SMISTATI, __SALTATI)
- Aggiunge pulsante "Anteprima saltati" in UI con anteprima e pulizia
- Aggiunge guida utente HTML/CSS in renderer/docs/ con sidebar navigabile
- Aggiunge menu Help > Documentazione che apre la guida in una finestra
- Imposta DEFAULT_DESTINATION a X:\
2026-03-16 12:10:54 +01:00
14 changed files with 503 additions and 77 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

54
main.js
View File

@@ -8,11 +8,13 @@ const {
resolveUnroutedDir, resolveUnroutedDir,
listUnroutedFiles, listUnroutedFiles,
listDuplicateFiles, listDuplicateFiles,
listSkippedFiles,
clearUnroutedFiles, clearUnroutedFiles,
clearDuplicateFiles, clearDuplicateFiles,
clearSkippedFiles,
} = require('./services/unrouted'); } = require('./services/unrouted');
const CAD_EXTENSIONS = ['prt', 'asm', 'dwr']; const CAD_EXTENSIONS = ['prt', 'asm', 'drw'];
const DEFAULT_DESTINATION = 'X:\\'; const DEFAULT_DESTINATION = 'X:\\';
const SETTINGS_FILENAME = 'cad-router-settings.json'; const SETTINGS_FILENAME = 'cad-router-settings.json';
const LINUX_RUNTIME_DIR = '.cadroute'; const LINUX_RUNTIME_DIR = '.cadroute';
@@ -66,8 +68,8 @@ let config = buildConfig(DEFAULT_DESTINATION);
function createWindow() { function createWindow() {
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 900, width: 1000,
height: 640, height: 800,
icon: path.join(__dirname, 'build', 'icon.png'), icon: path.join(__dirname, 'build', 'icon.png'),
webPreferences: { webPreferences: {
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
@@ -91,7 +93,32 @@ function createDocsWindow() {
docs.setMenuBarVisibility(false); docs.setMenuBarVisibility(false);
} }
function showAboutDialog() {
dialog.showMessageBox({
type: 'info',
title: 'Informazioni su CadRoute',
message: 'CadRoute',
detail: [
`Versione: ${app.getVersion()}`,
`Autore: Davide Grilli`,
`Azienda: Cevolani Italia s.r.l.`,
`Licenza: MIT`,
``,
`Smistatore automatico di file CAD (Creo).`,
``,
`Electron: ${process.versions.electron}`,
`Node: ${process.versions.node}`,
].join('\n'),
buttons: ['OK'],
noLink: true,
});
}
Menu.setApplicationMenu(Menu.buildFromTemplate([ Menu.setApplicationMenu(Menu.buildFromTemplate([
{
label: 'Informazioni',
submenu: [{ label: 'Informazioni su CadRoute...', click: () => showAboutDialog() }],
},
{ {
label: 'Help', label: 'Help',
submenu: [{ label: 'Documentazione', click: () => createDocsWindow() }], submenu: [{ label: 'Documentazione', click: () => createDocsWindow() }],
@@ -115,7 +142,7 @@ app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit(); if (process.platform !== 'darwin') app.quit();
}); });
ipcMain.handle('select-folder', async () => { ipcMain.handle('select-folder', async (event) => {
const result = await dialog.showOpenDialog({ const result = await dialog.showOpenDialog({
properties: ['openDirectory'], properties: ['openDirectory'],
}); });
@@ -124,11 +151,12 @@ ipcMain.handle('select-folder', async () => {
return { canceled: true }; 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 }; return { canceled: false, ...routingResult };
}); });
ipcMain.handle('select-zip', async () => { ipcMain.handle('select-zip', async (event) => {
const result = await dialog.showOpenDialog({ const result = await dialog.showOpenDialog({
properties: ['openFile'], properties: ['openFile'],
filters: [{ name: 'Zip', extensions: ['zip'] }], filters: [{ name: 'Zip', extensions: ['zip'] }],
@@ -138,11 +166,12 @@ ipcMain.handle('select-zip', async () => {
return { canceled: true }; 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 }; 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(); const droppedPath = String(payload?.path || '').trim();
if (!droppedPath) { if (!droppedPath) {
throw new Error('Percorso non valido'); throw new Error('Percorso non valido');
@@ -153,13 +182,15 @@ ipcMain.handle('process-dropped-path', async (_event, payload) => {
throw new Error('Percorso non trovato'); throw new Error('Percorso non trovato');
} }
const onProgress = (data) => event.sender.send('progress', data);
if (stats.isDirectory()) { if (stats.isDirectory()) {
const routingResult = await processFolder(droppedPath, config); const routingResult = await processFolder(droppedPath, config, onProgress);
return { canceled: false, sourceType: 'folder', ...routingResult }; return { canceled: false, sourceType: 'folder', ...routingResult };
} }
if (stats.isFile() && path.extname(droppedPath).toLowerCase() === '.zip') { 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 }; return { canceled: false, sourceType: 'zip', ...routingResult };
} }
@@ -196,6 +227,7 @@ ipcMain.handle('update-destination', async (_event, payload) => {
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);
@@ -209,5 +241,7 @@ ipcMain.handle('open-unrouted-folder', async () => {
ipcMain.handle('list-unrouted-files', async () => listUnroutedFiles()); ipcMain.handle('list-unrouted-files', async () => listUnroutedFiles());
ipcMain.handle('list-duplicates-files', async () => listDuplicateFiles()); ipcMain.handle('list-duplicates-files', async () => listDuplicateFiles());
ipcMain.handle('list-skipped-files', async () => listSkippedFiles());
ipcMain.handle('clear-unrouted-files', async () => clearUnroutedFiles()); ipcMain.handle('clear-unrouted-files', async () => clearUnroutedFiles());
ipcMain.handle('clear-duplicates-files', async () => clearDuplicateFiles()); ipcMain.handle('clear-duplicates-files', async () => clearDuplicateFiles());
ipcMain.handle('clear-skipped-files', async () => clearSkippedFiles());

View File

@@ -11,6 +11,10 @@ contextBridge.exposeInMainWorld('api', {
openUnroutedFolder: () => ipcRenderer.invoke('open-unrouted-folder'), openUnroutedFolder: () => ipcRenderer.invoke('open-unrouted-folder'),
listUnroutedFiles: () => ipcRenderer.invoke('list-unrouted-files'), listUnroutedFiles: () => ipcRenderer.invoke('list-unrouted-files'),
listDuplicatesFiles: () => ipcRenderer.invoke('list-duplicates-files'), listDuplicatesFiles: () => ipcRenderer.invoke('list-duplicates-files'),
listSkippedFiles: () => ipcRenderer.invoke('list-skipped-files'),
clearUnroutedFiles: () => ipcRenderer.invoke('clear-unrouted-files'), clearUnroutedFiles: () => ipcRenderer.invoke('clear-unrouted-files'),
clearDuplicatesFiles: () => ipcRenderer.invoke('clear-duplicates-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="#nomi">Convenzione nomi file</a>
<a href="#nonsmistati">File non smistati</a> <a href="#nonsmistati">File non smistati</a>
<a href="#duplicati">File duplicati</a> <a href="#duplicati">File duplicati</a>
<a href="#saltati">File saltati</a>
</nav> </nav>
<main> <main>
@@ -55,14 +56,14 @@
<td>File di assemblaggio Creo</td> <td>File di assemblaggio Creo</td>
</tr> </tr>
<tr> <tr>
<td><span class="badge dwr">.dwr</span></td> <td><span class="badge dwr">.drw</span></td>
<td>Drawing</td> <td>Drawing</td>
<td>File di disegno Creo</td> <td>File di disegno Creo</td>
</tr> </tr>
<tr> <tr>
<td><span class="badge skip">altri</span></td> <td><span class="badge skip">altri</span></td>
<td></td> <td></td>
<td>Ignorati (PDF, immagini, documenti, ecc.)</td> <td>Copiati in <code>__SALTATI</code> (PDF, immagini, documenti, ecc.)</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -100,7 +101,7 @@
<ol class="steps"> <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>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>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> <li><span>Al termine vengono mostrate le statistiche nell'area di output.</span></li>
</ol> </ol>
@@ -127,7 +128,7 @@
</div> </div>
<div class="stat-item muted"> <div class="stat-item muted">
<div class="stat-label">Saltati</div> <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>
<div class="stat-item warn"> <div class="stat-item warn">
<div class="stat-label">Non smistati</div> <div class="stat-label">Non smistati</div>
@@ -172,7 +173,7 @@ Esempi:
</tr> </tr>
<tr> <tr>
<td>File già presente in destinazione</td> <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>
<tr> <tr>
<td>Versione inferiore presente nella sorgente</td> <td>Versione inferiore presente nella sorgente</td>
@@ -210,8 +211,8 @@ Esempi:
<h3>Dove si trovano</h3> <h3>Dove si trovano</h3>
<p>I file non smistati vengono copiati nella cartella <code>__NON_SMISTATI</code>:</p> <p>I file non smistati vengono copiati nella cartella <code>__NON_SMISTATI</code>:</p>
<ul style="margin: 8px 0 12px 20px; color: var(--muted);"> <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>Linux:</strong> <code>~/.cadroute/__NON_SMISTATI/</code></li>
<li><strong>Windows:</strong> <code>%APPDATA%\CadRoute\__NON_SMISTATI\</code></li>
</ul> </ul>
<h3>Visualizzare e pulire</h3> <h3>Visualizzare e pulire</h3>
@@ -231,16 +232,16 @@ Esempi:
<h2>File duplicati</h2> <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>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> <h3>Dove si trovano</h3>
<ul style="margin: 8px 0 12px 20px; color: var(--muted);"> <ul style="margin: 8px 0 12px 20px; color: var(--muted);">
<li><strong>Linux:</strong> <code>~/.cadroute/duplicati/</code></li> <li><strong>Windows:</strong> <code>C:\cadroute\__DUPLICATI\</code></li>
<li><strong>Windows:</strong> <code>%APPDATA%\CadRoute\duplicati\</code></li> <li><strong>Linux:</strong> <code>~/.cadroute/__DUPLICATI/</code></li>
</ul> </ul>
<h3>Gestione versioni nei duplicati</h3> <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> <h3>Visualizzare e pulire</h3>
<ol class="steps"> <ol class="steps">
@@ -254,6 +255,31 @@ Esempi:
</div> </div>
</section> </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> </main>
<script> <script>

View File

@@ -24,32 +24,35 @@
background: linear-gradient(130deg, #eaf0ff 0%, var(--bg) 45%, #f9fbff 100%); background: linear-gradient(130deg, #eaf0ff 0%, var(--bg) 45%, #f9fbff 100%);
color: var(--text); color: var(--text);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 16px;
} }
.page { .page {
max-width: 820px; max-width: 980px;
margin: 40px auto; margin: 40px auto;
background: var(--card); background: var(--card);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 14px; border-radius: 14px;
padding: 26px; padding: 32px;
} }
h1 { h1 {
margin-top: 0; margin-top: 0;
margin-bottom: 6px; margin-bottom: 8px;
font-size: 26px;
} }
p { p {
margin: 0; margin: 0;
color: var(--muted); color: var(--muted);
font-size: 15px;
} }
.actions { .actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 14px;
margin-top: 18px; margin-top: 22px;
} }
button { button {
@@ -57,8 +60,8 @@
background: var(--accent); background: var(--accent);
color: white; color: white;
border-radius: 10px; border-radius: 10px;
padding: 11px 16px; padding: 13px 20px;
font-size: 15px; font-size: 16px;
cursor: pointer; cursor: pointer;
} }
@@ -116,43 +119,43 @@
} }
.rules h2 { .rules h2 {
margin: 0 0 10px; margin: 0 0 12px;
font-size: 17px; font-size: 19px;
} }
.rule-row { .rule-row {
display: grid; display: grid;
grid-template-columns: 180px 1fr auto auto; grid-template-columns: 180px 1fr auto auto;
gap: 8px; gap: 10px;
align-items: center; align-items: center;
margin-bottom: 8px; margin-bottom: 10px;
} }
.rule-row.single-row { .rule-row.single-row {
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto;
margin-top: 10px; margin-top: 12px;
} }
.rule-row.preview-actions { .rule-row.preview-actions {
grid-template-columns: auto auto; grid-template-columns: auto auto auto;
justify-content: start; justify-content: start;
} }
.rule-label { .rule-label {
font-size: 13px; font-size: 15px;
color: var(--muted); color: var(--muted);
} }
.rule-row input { .rule-row input {
min-width: 0; min-width: 0;
padding: 9px 10px; padding: 11px 12px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 16px;
} }
.rule-row button { .rule-row button {
padding: 9px 12px; padding: 11px 16px;
border-radius: 8px; border-radius: 8px;
} }
@@ -162,7 +165,7 @@
.rule-status { .rule-status {
margin-top: 4px; margin-top: 4px;
font-size: 12px; font-size: 14px;
color: var(--muted); color: var(--muted);
} }
@@ -251,6 +254,53 @@
overflow: auto; overflow: auto;
min-height: 180px; 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> </style>
</head> </head>
<body> <body>
@@ -267,6 +317,11 @@
Trascina qui una cartella o un file .zip Trascina qui una cartella o un file .zip
</section> </section>
<div class="progress-container" id="progressContainer">
<div class="progress-label" id="progressLabel">In corso...</div>
<progress id="progressBar"></progress>
</div>
<section class="rules"> <section class="rules">
<h2>Destinazione file CAD</h2> <h2>Destinazione file CAD</h2>
<div class="rule-row single-row"> <div class="rule-row single-row">
@@ -277,6 +332,7 @@
<div class="rule-row single-row preview-actions"> <div class="rule-row single-row preview-actions">
<button id="openUnroutedBtn" class="browse">Anteprima non smistati</button> <button id="openUnroutedBtn" class="browse">Anteprima non smistati</button>
<button id="openDuplicatesBtn" class="browse">Anteprima duplicati</button> <button id="openDuplicatesBtn" class="browse">Anteprima duplicati</button>
<button id="openSkippedBtn" class="browse">Anteprima saltati</button>
</div> </div>
<div class="rule-status" id="destinationStatus"></div> <div class="rule-status" id="destinationStatus"></div>
</section> </section>

View File

@@ -7,6 +7,7 @@ const browseDestinationBtn = document.getElementById('browseDestinationBtn');
const saveDestinationBtn = document.getElementById('saveDestinationBtn'); const saveDestinationBtn = document.getElementById('saveDestinationBtn');
const openUnroutedBtn = document.getElementById('openUnroutedBtn'); const openUnroutedBtn = document.getElementById('openUnroutedBtn');
const openDuplicatesBtn = document.getElementById('openDuplicatesBtn'); const openDuplicatesBtn = document.getElementById('openDuplicatesBtn');
const openSkippedBtn = document.getElementById('openSkippedBtn');
const destinationStatus = document.getElementById('destinationStatus'); const destinationStatus = document.getElementById('destinationStatus');
const previewOverlay = document.getElementById('previewOverlay'); const previewOverlay = document.getElementById('previewOverlay');
const closePreviewBtn = document.getElementById('closePreviewBtn'); const closePreviewBtn = document.getElementById('closePreviewBtn');
@@ -14,10 +15,52 @@ const clearPreviewBtn = document.getElementById('clearPreviewBtn');
const previewTitle = document.getElementById('previewTitle'); const previewTitle = document.getElementById('previewTitle');
const previewMeta = document.getElementById('previewMeta'); const previewMeta = document.getElementById('previewMeta');
const previewList = document.getElementById('previewList'); 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'; const defaultDropZoneText = 'Trascina qui una cartella o un file .zip';
let isProcessing = false; let isProcessing = false;
let currentPreviewKind = null; 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) { function setLoading(isLoading) {
folderBtn.disabled = isLoading; folderBtn.disabled = isLoading;
zipBtn.disabled = isLoading; zipBtn.disabled = isLoading;
@@ -29,6 +72,7 @@ function setDestinationLoading(isLoading) {
saveDestinationBtn.disabled = isLoading; saveDestinationBtn.disabled = isLoading;
openUnroutedBtn.disabled = isLoading; openUnroutedBtn.disabled = isLoading;
openDuplicatesBtn.disabled = isLoading; openDuplicatesBtn.disabled = isLoading;
openSkippedBtn.disabled = isLoading;
clearPreviewBtn.disabled = isLoading; clearPreviewBtn.disabled = isLoading;
} }
@@ -60,18 +104,14 @@ function hidePreview() {
} }
async function loadPreviewData(kind) { async function loadPreviewData(kind) {
if (kind === 'duplicates') { if (kind === 'duplicates') return window.api.listDuplicatesFiles();
return window.api.listDuplicatesFiles(); if (kind === 'skipped') return window.api.listSkippedFiles();
}
return window.api.listUnroutedFiles(); return window.api.listUnroutedFiles();
} }
async function clearPreviewData(kind) { async function clearPreviewData(kind) {
if (kind === 'duplicates') { if (kind === 'duplicates') return window.api.clearDuplicatesFiles();
return window.api.clearDuplicatesFiles(); if (kind === 'skipped') return window.api.clearSkippedFiles();
}
return window.api.clearUnroutedFiles(); return window.api.clearUnroutedFiles();
} }
@@ -103,6 +143,8 @@ async function handleAction(actionName, actionFn) {
isProcessing = true; isProcessing = true;
setLoading(true); setLoading(true);
showProgress();
window.api.onProgress(updateProgress);
output.textContent = `${actionName} in corso...`; output.textContent = `${actionName} in corso...`;
try { try {
@@ -117,6 +159,8 @@ async function handleAction(actionName, actionFn) {
} catch (error) { } catch (error) {
output.textContent = `${actionName} fallito:\n${error.message}`; output.textContent = `${actionName} fallito:\n${error.message}`;
} finally { } finally {
window.api.offProgress();
hideProgress();
setLoading(false); setLoading(false);
isProcessing = false; isProcessing = false;
} }
@@ -248,7 +292,22 @@ openDuplicatesBtn.addEventListener('click', async () => {
destinationStatus.textContent = 'Caricamento anteprima duplicati...'; destinationStatus.textContent = 'Caricamento anteprima duplicati...';
const result = await window.api.listDuplicatesFiles(); const result = await window.api.listDuplicatesFiles();
showPreview('duplicates', 'Anteprima duplicati', result, 'Nessun file presente in duplicati.'); showPreview('duplicates', 'Anteprima __DUPLICATI', result, 'Nessun file presente in __DUPLICATI.');
destinationStatus.textContent = `Anteprima caricata (${result.files?.length || 0} file).`;
} catch (error) {
destinationStatus.textContent = `Errore: ${error.message}`;
} finally {
setDestinationLoading(false);
}
});
openSkippedBtn.addEventListener('click', async () => {
try {
setDestinationLoading(true);
destinationStatus.textContent = 'Caricamento anteprima saltati...';
const result = await window.api.listSkippedFiles();
showPreview('skipped', 'Anteprima __SALTATI', result, 'Nessun file presente in __SALTATI.');
destinationStatus.textContent = `Anteprima caricata (${result.files?.length || 0} file).`; destinationStatus.textContent = `Anteprima caricata (${result.files?.length || 0} file).`;
} catch (error) { } catch (error) {
destinationStatus.textContent = `Errore: ${error.message}`; destinationStatus.textContent = `Errore: ${error.message}`;
@@ -263,8 +322,11 @@ clearPreviewBtn.addEventListener('click', async () => {
} }
const isDuplicates = currentPreviewKind === 'duplicates'; const isDuplicates = currentPreviewKind === 'duplicates';
const isSkipped = currentPreviewKind === 'skipped';
const confirmMessage = isDuplicates const confirmMessage = isDuplicates
? 'Confermi la pulizia della cartella duplicati?' ? 'Confermi la pulizia della cartella __DUPLICATI?'
: isSkipped
? 'Confermi la pulizia della cartella saltati?'
: 'Confermi la pulizia della cartella non smistati?'; : 'Confermi la pulizia della cartella non smistati?';
if (!window.confirm(confirmMessage)) { if (!window.confirm(confirmMessage)) {
@@ -277,9 +339,11 @@ clearPreviewBtn.addEventListener('click', async () => {
const clearResult = await clearPreviewData(currentPreviewKind); const clearResult = await clearPreviewData(currentPreviewKind);
const refreshed = await loadPreviewData(currentPreviewKind); const refreshed = await loadPreviewData(currentPreviewKind);
const title = isDuplicates ? 'Anteprima duplicati' : 'Anteprima __NON_SMISTATI'; const title = isDuplicates ? 'Anteprima __DUPLICATI' : isSkipped ? 'Anteprima __SALTATI' : 'Anteprima __NON_SMISTATI';
const emptyMessage = isDuplicates const emptyMessage = isDuplicates
? 'Nessun file presente in duplicati.' ? 'Nessun file presente in __DUPLICATI.'
: isSkipped
? 'Nessun file presente in __SALTATI.'
: 'Nessun file presente in __NON_SMISTATI.'; : 'Nessun file presente in __NON_SMISTATI.';
showPreview(currentPreviewKind, title, refreshed, emptyMessage); showPreview(currentPreviewKind, title, refreshed, emptyMessage);
@@ -291,6 +355,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) {

View File

@@ -4,13 +4,15 @@ const path = require('path');
const CODE_FOLDER_REGEX = /^\d{3}$/; const CODE_FOLDER_REGEX = /^\d{3}$/;
const MAX_SCAN_DEPTH = 6; const MAX_SCAN_DEPTH = 6;
async function buildDestinationIndex(destinationRoot) { async function buildDestinationIndex(destinationRoot, onProgress) {
const index = new Map(); const index = new Map();
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) { if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
return index; return index;
} }
let scanned = 0;
async function walk(currentDir, depth) { async function walk(currentDir, depth) {
if (depth > MAX_SCAN_DEPTH) { if (depth > MAX_SCAN_DEPTH) {
return; return;
@@ -29,6 +31,9 @@ async function buildDestinationIndex(destinationRoot) {
} }
const fullPath = path.join(currentDir, entry.name); const fullPath = path.join(currentDir, entry.name);
scanned += 1;
onProgress?.({ phase: 'index-dest', scanned, folder: entry.name });
if (CODE_FOLDER_REGEX.test(entry.name)) { if (CODE_FOLDER_REGEX.test(entry.name)) {
const rows = index.get(entry.name) || []; const rows = index.get(entry.name) || [];
rows.push(fullPath); 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(); return String(cadInfo?.key || '').toLowerCase();
} }
async function buildExistingCadKeyIndex(destinationRoot) { async function buildExistingCadKeyIndex(destinationRoot, onProgress) {
const keys = new Set(); const keys = new Set();
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) { if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
return keys; return keys;
} }
let scanned = 0;
async function walk(currentDir, depth) { async function walk(currentDir, depth) {
if (depth > MAX_SCAN_DEPTH) { if (depth > MAX_SCAN_DEPTH) {
return; return;
@@ -44,6 +46,8 @@ async function buildExistingCadKeyIndex(destinationRoot) {
continue; continue;
} }
scanned += 1;
onProgress?.({ phase: 'index-dup', scanned, file: entry.name });
keys.add(toCadKey(cadInfo)); keys.add(toCadKey(cadInfo));
} }
} }

View File

@@ -1,9 +1,8 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const { getDestinationDecision, getCadInfo } = require('./router'); const { getDestinationDecision, getCadInfo } = require('./router');
const { buildDestinationIndex } = require('./destinationIndex'); const { buildDestinationIndexes, toCadKey } = require('./destinationScanner');
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex'); const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted');
const { prepareUnroutedTarget, prepareDuplicateTarget } = require('./unrouted');
function parseNumericVersion(version) { function parseNumericVersion(version) {
const rawVersion = String(version || '').trim(); const rawVersion = String(version || '').trim();
@@ -66,10 +65,11 @@ async function collectFilesRecursively(rootDir) {
return files; return files;
} }
async function processFolder(folder, config) { async function processFolder(folder, config, onProgress) {
onProgress?.({ phase: 'scan' });
const files = await collectFilesRecursively(folder); 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 sourceMaxVersions = buildHighestNumericVersionByCadKey(files);
const result = { const result = {
scanned: 0, scanned: 0,
@@ -80,13 +80,20 @@ async function processFolder(folder, config) {
details: [], 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; const file = fileName;
result.scanned += 1; result.scanned += 1;
onProgress?.({ phase: 'copy', current: i + 1, total, file });
const cadInfo = getCadInfo(file); const cadInfo = getCadInfo(file);
if (!cadInfo) { if (!cadInfo) {
const skippedTarget = await getSkippedTarget(file);
await fs.copy(fullPath, skippedTarget.destinationPath);
result.skipped += 1; result.skipped += 1;
result.copied += 1;
continue; continue;
} }

View File

@@ -1,13 +1,12 @@
const path = require('path'); const path = require('path');
function normalizeCadType(ext) { function normalizeCadType(ext) {
const lowerExt = String(ext || '').toLowerCase(); return String(ext || '').toLowerCase();
return lowerExt === 'drw' ? 'dwr' : lowerExt;
} }
function getCadInfo(filename) { function getCadInfo(filename) {
const baseName = path.basename(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) { if (!match) {
return null; return null;
} }
@@ -17,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,
@@ -25,6 +27,7 @@ function getCadInfo(filename) {
version, version,
key: `${rawCode}.${type}`, key: `${rawCode}.${type}`,
routingGroup, 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) { function findDestination(filename, config, destinationIndex) {

View File

@@ -5,8 +5,10 @@ const { getCadInfo } = require('./router');
const PRIMARY_UNROUTED_DIR = '/cadroute/__NON_SMISTATI'; const PRIMARY_UNROUTED_DIR = '/cadroute/__NON_SMISTATI';
const HOME_UNROUTED_DIR = path.join(os.homedir(), '.cadroute', '__NON_SMISTATI'); const HOME_UNROUTED_DIR = path.join(os.homedir(), '.cadroute', '__NON_SMISTATI');
const PRIMARY_DUPLICATES_DIR = '/cadroute/duplicati'; const PRIMARY_DUPLICATES_DIR = '/cadroute/__DUPLICATI';
const HOME_DUPLICATES_DIR = path.join(os.homedir(), '.cadroute', 'duplicati'); const HOME_DUPLICATES_DIR = path.join(os.homedir(), '.cadroute', '__DUPLICATI');
const PRIMARY_SKIPPED_DIR = '/cadroute/__SALTATI';
const HOME_SKIPPED_DIR = path.join(os.homedir(), '.cadroute', '__SALTATI');
const SPECIAL_TARGETS = { const SPECIAL_TARGETS = {
unrouted: { unrouted: {
@@ -17,6 +19,10 @@ const SPECIAL_TARGETS = {
primary: PRIMARY_DUPLICATES_DIR, primary: PRIMARY_DUPLICATES_DIR,
fallback: HOME_DUPLICATES_DIR, fallback: HOME_DUPLICATES_DIR,
}, },
skipped: {
primary: PRIMARY_SKIPPED_DIR,
fallback: HOME_SKIPPED_DIR,
},
}; };
const resolvedDirs = new Map(); const resolvedDirs = new Map();
@@ -178,6 +184,22 @@ async function prepareDuplicateTarget(fileName) {
return prepareSpecialTarget('duplicates', fileName); return prepareSpecialTarget('duplicates', fileName);
} }
async function resolveSkippedDir() {
return resolveTargetDir('skipped');
}
async function getSkippedTarget(fileName) {
return getTarget('skipped', fileName);
}
async function listSkippedFiles() {
return listTargetFiles('skipped');
}
async function clearSkippedFiles() {
return clearTargetFiles('skipped');
}
async function listFilesRecursively(rootDir) { async function listFilesRecursively(rootDir) {
const files = []; const files = [];
@@ -251,16 +273,22 @@ async function clearDuplicateFiles() {
module.exports = { module.exports = {
getUnroutedTarget, getUnroutedTarget,
getDuplicateTarget, getDuplicateTarget,
getSkippedTarget,
prepareUnroutedTarget, prepareUnroutedTarget,
prepareDuplicateTarget, prepareDuplicateTarget,
resolveUnroutedDir, resolveUnroutedDir,
resolveDuplicatesDir, resolveDuplicatesDir,
resolveSkippedDir,
listUnroutedFiles, listUnroutedFiles,
listDuplicateFiles, listDuplicateFiles,
listSkippedFiles,
clearUnroutedFiles, clearUnroutedFiles,
clearDuplicateFiles, clearDuplicateFiles,
clearSkippedFiles,
PRIMARY_UNROUTED_DIR, PRIMARY_UNROUTED_DIR,
HOME_UNROUTED_DIR, HOME_UNROUTED_DIR,
PRIMARY_DUPLICATES_DIR, PRIMARY_DUPLICATES_DIR,
HOME_DUPLICATES_DIR, HOME_DUPLICATES_DIR,
PRIMARY_SKIPPED_DIR,
HOME_SKIPPED_DIR,
}; };

View File

@@ -3,9 +3,8 @@ const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const { pipeline } = require('stream/promises'); const { pipeline } = require('stream/promises');
const { getDestinationDecision, getCadInfo } = require('./router'); const { getDestinationDecision, getCadInfo } = require('./router');
const { buildDestinationIndex } = require('./destinationIndex'); const { buildDestinationIndexes, toCadKey } = require('./destinationScanner');
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex'); const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted');
const { prepareUnroutedTarget, prepareDuplicateTarget } = require('./unrouted');
function parseNumericVersion(version) { function parseNumericVersion(version) {
const rawVersion = String(version || '').trim(); const rawVersion = String(version || '').trim();
@@ -27,7 +26,7 @@ async function buildZipHighestNumericVersionByCadKey(zipPath) {
try { try {
directory = await unzipper.Open.file(zipPath); directory = await unzipper.Open.file(zipPath);
} catch { } catch {
return index; return { index, total: 0 };
} }
for (const row of directory.files || []) { 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 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 = { const result = {
scanned: 0, scanned: 0,
copied: 0, copied: 0,
@@ -70,6 +71,8 @@ async function processZip(zipPath, config) {
details: [], details: [],
}; };
let current = 0;
for await (const entry of stream) { for await (const entry of stream) {
if (entry.type !== 'File') { if (entry.type !== 'File') {
entry.autodrain(); entry.autodrain();
@@ -78,12 +81,16 @@ async function processZip(zipPath, config) {
const file = entry.path; const file = entry.path;
const baseName = path.basename(file); const baseName = path.basename(file);
current += 1;
result.scanned += 1; result.scanned += 1;
onProgress?.({ phase: 'copy', current, total, file: baseName });
const cadInfo = getCadInfo(baseName); const cadInfo = getCadInfo(baseName);
if (!cadInfo) { if (!cadInfo) {
const skippedTarget = await getSkippedTarget(baseName);
await pipeline(entry, fs.createWriteStream(skippedTarget.destinationPath));
result.skipped += 1; result.skipped += 1;
entry.autodrain(); result.copied += 1;
continue; continue;
} }