Compare commits
5 Commits
0612cb0d8a
...
d529e34249
| Author | SHA1 | Date | |
|---|---|---|---|
| d529e34249 | |||
| 5e4e03a103 | |||
| 3a567c390c | |||
| 164959acaf | |||
| 771891ac4f |
41
main.js
41
main.js
@@ -1,13 +1,21 @@
|
|||||||
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
|
const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
const { processFolder } = require('./services/folderProcessor');
|
const { processFolder } = require('./services/folderProcessor');
|
||||||
const { processZip } = require('./services/zipProcessor');
|
const { processZip } = require('./services/zipProcessor');
|
||||||
|
const {
|
||||||
|
resolveUnroutedDir,
|
||||||
|
listUnroutedFiles,
|
||||||
|
listDuplicateFiles,
|
||||||
|
clearUnroutedFiles,
|
||||||
|
clearDuplicateFiles,
|
||||||
|
} = require('./services/unrouted');
|
||||||
|
|
||||||
const CAD_EXTENSIONS = ['prt', 'asm', 'dwr'];
|
const CAD_EXTENSIONS = ['prt', 'asm', 'dwr'];
|
||||||
const DEFAULT_DESTINATION = './output/cad';
|
const DEFAULT_DESTINATION = './output/cad';
|
||||||
const SETTINGS_FILENAME = 'cad-router-settings.json';
|
const SETTINGS_FILENAME = 'cad-router-settings.json';
|
||||||
|
const LINUX_RUNTIME_DIR = '.cadroute';
|
||||||
|
|
||||||
function buildConfig(destination) {
|
function buildConfig(destination) {
|
||||||
const resolvedDestination = String(destination || '').trim() || DEFAULT_DESTINATION;
|
const resolvedDestination = String(destination || '').trim() || DEFAULT_DESTINATION;
|
||||||
@@ -18,7 +26,19 @@ function buildConfig(destination) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSettingsPath() {
|
function getSettingsPath() {
|
||||||
return path.join(app.getPath('userData'), SETTINGS_FILENAME);
|
return path.join(getRuntimeDirectory(), SETTINGS_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuntimeDirectory() {
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
return path.join(app.getPath('home'), LINUX_RUNTIME_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.getPath('userData');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureRuntimeDirectory() {
|
||||||
|
await fs.ensureDir(getRuntimeDirectory());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPersistedDestination() {
|
async function loadPersistedDestination() {
|
||||||
@@ -59,6 +79,7 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
await ensureRuntimeDirectory();
|
||||||
const persistedDestination = await loadPersistedDestination();
|
const persistedDestination = await loadPersistedDestination();
|
||||||
config = buildConfig(persistedDestination);
|
config = buildConfig(persistedDestination);
|
||||||
createWindow();
|
createWindow();
|
||||||
@@ -154,3 +175,19 @@ ipcMain.handle('update-destination', async (_event, payload) => {
|
|||||||
await persistDestination(config.destination);
|
await persistDestination(config.destination);
|
||||||
return { destination: config.destination };
|
return { destination: config.destination };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('open-unrouted-folder', async () => {
|
||||||
|
const unroutedDir = await resolveUnroutedDir();
|
||||||
|
const openResult = await shell.openPath(unroutedDir);
|
||||||
|
|
||||||
|
if (openResult) {
|
||||||
|
throw new Error(`Impossibile aprire la cartella: ${openResult}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { path: unroutedDir };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('list-unrouted-files', async () => listUnroutedFiles());
|
||||||
|
ipcMain.handle('list-duplicates-files', async () => listDuplicateFiles());
|
||||||
|
ipcMain.handle('clear-unrouted-files', async () => clearUnroutedFiles());
|
||||||
|
ipcMain.handle('clear-duplicates-files', async () => clearDuplicateFiles());
|
||||||
|
|||||||
@@ -8,4 +8,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
getDestination: () => ipcRenderer.invoke('get-destination'),
|
getDestination: () => ipcRenderer.invoke('get-destination'),
|
||||||
selectDestinationFolder: () => ipcRenderer.invoke('select-destination-folder'),
|
selectDestinationFolder: () => ipcRenderer.invoke('select-destination-folder'),
|
||||||
updateDestination: (destination) => ipcRenderer.invoke('update-destination', { destination }),
|
updateDestination: (destination) => ipcRenderer.invoke('update-destination', { destination }),
|
||||||
|
openUnroutedFolder: () => ipcRenderer.invoke('open-unrouted-folder'),
|
||||||
|
listUnroutedFiles: () => ipcRenderer.invoke('list-unrouted-files'),
|
||||||
|
listDuplicatesFiles: () => ipcRenderer.invoke('list-duplicates-files'),
|
||||||
|
clearUnroutedFiles: () => ipcRenderer.invoke('clear-unrouted-files'),
|
||||||
|
clearDuplicatesFiles: () => ipcRenderer.invoke('clear-duplicates-files'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -133,6 +133,11 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule-row.preview-actions {
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
.rule-label {
|
.rule-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -161,6 +166,75 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.55);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-overlay.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card {
|
||||||
|
width: min(900px, 100%);
|
||||||
|
max-height: 85vh;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
background: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-list {
|
||||||
|
margin: 0;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 220px;
|
||||||
|
max-height: 58vh;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.rule-row {
|
.rule-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -200,11 +274,29 @@
|
|||||||
<button id="browseDestinationBtn" class="browse">Sfoglia</button>
|
<button id="browseDestinationBtn" class="browse">Sfoglia</button>
|
||||||
<button id="saveDestinationBtn">Salva</button>
|
<button id="saveDestinationBtn">Salva</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="rule-row single-row preview-actions">
|
||||||
|
<button id="openUnroutedBtn" class="browse">Anteprima non smistati</button>
|
||||||
|
<button id="openDuplicatesBtn" class="browse">Anteprima duplicati</button>
|
||||||
|
</div>
|
||||||
<div class="rule-status" id="destinationStatus"></div>
|
<div class="rule-status" id="destinationStatus"></div>
|
||||||
</section>
|
</section>
|
||||||
<pre id="output">Pronto.</pre>
|
<pre id="output">Pronto.</pre>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<section class="preview-overlay" id="previewOverlay">
|
||||||
|
<article class="preview-card">
|
||||||
|
<div class="preview-header">
|
||||||
|
<h2 id="previewTitle">Anteprima</h2>
|
||||||
|
<div class="preview-header-actions">
|
||||||
|
<button id="clearPreviewBtn" class="danger">Pulisci cartella</button>
|
||||||
|
<button id="closePreviewBtn" class="secondary">Chiudi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-meta" id="previewMeta"></div>
|
||||||
|
<pre class="preview-list" id="previewList">Nessun dato.</pre>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
<script src="./renderer.js"></script>
|
<script src="./renderer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,9 +5,18 @@ const output = document.getElementById('output');
|
|||||||
const destinationInput = document.getElementById('destinationInput');
|
const destinationInput = document.getElementById('destinationInput');
|
||||||
const browseDestinationBtn = document.getElementById('browseDestinationBtn');
|
const browseDestinationBtn = document.getElementById('browseDestinationBtn');
|
||||||
const saveDestinationBtn = document.getElementById('saveDestinationBtn');
|
const saveDestinationBtn = document.getElementById('saveDestinationBtn');
|
||||||
|
const openUnroutedBtn = document.getElementById('openUnroutedBtn');
|
||||||
|
const openDuplicatesBtn = document.getElementById('openDuplicatesBtn');
|
||||||
const destinationStatus = document.getElementById('destinationStatus');
|
const destinationStatus = document.getElementById('destinationStatus');
|
||||||
|
const previewOverlay = document.getElementById('previewOverlay');
|
||||||
|
const closePreviewBtn = document.getElementById('closePreviewBtn');
|
||||||
|
const clearPreviewBtn = document.getElementById('clearPreviewBtn');
|
||||||
|
const previewTitle = document.getElementById('previewTitle');
|
||||||
|
const previewMeta = document.getElementById('previewMeta');
|
||||||
|
const previewList = document.getElementById('previewList');
|
||||||
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;
|
||||||
|
|
||||||
function setLoading(isLoading) {
|
function setLoading(isLoading) {
|
||||||
folderBtn.disabled = isLoading;
|
folderBtn.disabled = isLoading;
|
||||||
@@ -18,6 +27,52 @@ function setDestinationLoading(isLoading) {
|
|||||||
destinationInput.disabled = isLoading;
|
destinationInput.disabled = isLoading;
|
||||||
browseDestinationBtn.disabled = isLoading;
|
browseDestinationBtn.disabled = isLoading;
|
||||||
saveDestinationBtn.disabled = isLoading;
|
saveDestinationBtn.disabled = isLoading;
|
||||||
|
openUnroutedBtn.disabled = isLoading;
|
||||||
|
openDuplicatesBtn.disabled = isLoading;
|
||||||
|
clearPreviewBtn.disabled = isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
const value = Number(bytes || 0);
|
||||||
|
if (value < 1024) return `${value} B`;
|
||||||
|
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
|
||||||
|
if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
return `${(value / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPreview(kind, title, payload, emptyMessage) {
|
||||||
|
const files = payload?.files || [];
|
||||||
|
const directory = payload?.directory || '';
|
||||||
|
const lines = files.length
|
||||||
|
? files.map((file) => `- ${file.relativePath} | ${formatBytes(file.size)}`).join('\n')
|
||||||
|
: emptyMessage;
|
||||||
|
|
||||||
|
currentPreviewKind = kind;
|
||||||
|
previewTitle.textContent = title;
|
||||||
|
previewMeta.textContent = `cartella: ${directory} | file totali: ${files.length}`;
|
||||||
|
previewList.textContent = lines;
|
||||||
|
previewOverlay.classList.add('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePreview() {
|
||||||
|
currentPreviewKind = null;
|
||||||
|
previewOverlay.classList.remove('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPreviewData(kind) {
|
||||||
|
if (kind === 'duplicates') {
|
||||||
|
return window.api.listDuplicatesFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.api.listUnroutedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearPreviewData(kind) {
|
||||||
|
if (kind === 'duplicates') {
|
||||||
|
return window.api.clearDuplicatesFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.api.clearUnroutedFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResult(title, result) {
|
function renderResult(title, result) {
|
||||||
@@ -33,6 +88,8 @@ function renderResult(title, result) {
|
|||||||
`scansionati: ${result.scanned ?? 0}`,
|
`scansionati: ${result.scanned ?? 0}`,
|
||||||
`copiati: ${result.copied ?? 0}`,
|
`copiati: ${result.copied ?? 0}`,
|
||||||
`saltati: ${result.skipped ?? 0}`,
|
`saltati: ${result.skipped ?? 0}`,
|
||||||
|
`non smistati: ${result.unrouted ?? 0}`,
|
||||||
|
`duplicati: ${result.duplicates ?? 0}`,
|
||||||
'',
|
'',
|
||||||
'dettagli (max 20):',
|
'dettagli (max 20):',
|
||||||
detailsText,
|
detailsText,
|
||||||
@@ -165,6 +222,88 @@ saveDestinationBtn.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
openUnroutedBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
setDestinationLoading(true);
|
||||||
|
destinationStatus.textContent = 'Caricamento anteprima non smistati...';
|
||||||
|
|
||||||
|
const result = await window.api.listUnroutedFiles();
|
||||||
|
showPreview(
|
||||||
|
'unrouted',
|
||||||
|
'Anteprima __NON_SMISTATI',
|
||||||
|
result,
|
||||||
|
'Nessun file presente in __NON_SMISTATI.'
|
||||||
|
);
|
||||||
|
destinationStatus.textContent = `Anteprima caricata (${result.files?.length || 0} file).`;
|
||||||
|
} catch (error) {
|
||||||
|
destinationStatus.textContent = `Errore: ${error.message}`;
|
||||||
|
} finally {
|
||||||
|
setDestinationLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
openDuplicatesBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
setDestinationLoading(true);
|
||||||
|
destinationStatus.textContent = 'Caricamento anteprima duplicati...';
|
||||||
|
|
||||||
|
const result = await window.api.listDuplicatesFiles();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clearPreviewBtn.addEventListener('click', async () => {
|
||||||
|
if (!currentPreviewKind) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDuplicates = currentPreviewKind === 'duplicates';
|
||||||
|
const confirmMessage = isDuplicates
|
||||||
|
? 'Confermi la pulizia della cartella duplicati?'
|
||||||
|
: 'Confermi la pulizia della cartella non smistati?';
|
||||||
|
|
||||||
|
if (!window.confirm(confirmMessage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setDestinationLoading(true);
|
||||||
|
destinationStatus.textContent = 'Pulizia cartella in corso...';
|
||||||
|
|
||||||
|
const clearResult = await clearPreviewData(currentPreviewKind);
|
||||||
|
const refreshed = await loadPreviewData(currentPreviewKind);
|
||||||
|
const title = isDuplicates ? 'Anteprima duplicati' : 'Anteprima __NON_SMISTATI';
|
||||||
|
const emptyMessage = isDuplicates
|
||||||
|
? 'Nessun file presente in duplicati.'
|
||||||
|
: 'Nessun file presente in __NON_SMISTATI.';
|
||||||
|
|
||||||
|
showPreview(currentPreviewKind, title, refreshed, emptyMessage);
|
||||||
|
destinationStatus.textContent = `Cartella pulita: ${clearResult.directory}`;
|
||||||
|
} catch (error) {
|
||||||
|
destinationStatus.textContent = `Errore: ${error.message}`;
|
||||||
|
} finally {
|
||||||
|
setDestinationLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closePreviewBtn.addEventListener('click', hidePreview);
|
||||||
|
previewOverlay.addEventListener('click', (event) => {
|
||||||
|
if (event.target === previewOverlay) {
|
||||||
|
hidePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape' && previewOverlay.classList.contains('visible')) {
|
||||||
|
hidePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function initConfigUI() {
|
async function initConfigUI() {
|
||||||
try {
|
try {
|
||||||
const destinationResult = await window.api.getDestination();
|
const destinationResult = await window.api.getDestination();
|
||||||
|
|||||||
55
services/duplicateIndex.js
Normal file
55
services/duplicateIndex.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const { getCadInfo } = require('./router');
|
||||||
|
|
||||||
|
const MAX_SCAN_DEPTH = 8;
|
||||||
|
|
||||||
|
function toCadKey(cadInfo) {
|
||||||
|
return String(cadInfo?.key || '').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildExistingCadKeyIndex(destinationRoot) {
|
||||||
|
const keys = new Set();
|
||||||
|
|
||||||
|
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function walk(currentDir, depth) {
|
||||||
|
if (depth > MAX_SCAN_DEPTH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(currentDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await walk(fullPath, depth + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cadInfo = getCadInfo(entry.name);
|
||||||
|
if (!cadInfo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.add(toCadKey(cadInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await walk(destinationRoot, 0);
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { buildExistingCadKeyIndex, toCadKey };
|
||||||
@@ -1,43 +1,90 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { getDestinationDecision, isCadFile } = require('./router');
|
const { getDestinationDecision, getCadInfo } = require('./router');
|
||||||
const { buildDestinationIndex } = require('./destinationIndex');
|
const { buildDestinationIndex } = require('./destinationIndex');
|
||||||
|
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
|
||||||
|
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
|
||||||
|
|
||||||
|
async function collectFilesRecursively(rootDir) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
async function walk(currentDir) {
|
||||||
|
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(currentDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await walk(fullPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isFile()) {
|
||||||
|
files.push({ fullPath, fileName: entry.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await walk(rootDir);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
async function processFolder(folder, config) {
|
async function processFolder(folder, config) {
|
||||||
const entries = await fs.readdir(folder, { withFileTypes: true });
|
const files = await collectFilesRecursively(folder);
|
||||||
const destinationIndex = await buildDestinationIndex(config?.destination);
|
const destinationIndex = await buildDestinationIndex(config?.destination);
|
||||||
|
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
|
||||||
const result = {
|
const result = {
|
||||||
scanned: 0,
|
scanned: 0,
|
||||||
copied: 0,
|
copied: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
|
unrouted: 0,
|
||||||
|
duplicates: 0,
|
||||||
details: [],
|
details: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const { fullPath, fileName } of files) {
|
||||||
if (!entry.isFile()) {
|
const file = fileName;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = entry.name;
|
|
||||||
result.scanned += 1;
|
result.scanned += 1;
|
||||||
|
|
||||||
if (!isCadFile(file)) {
|
const cadInfo = getCadInfo(file);
|
||||||
|
if (!cadInfo) {
|
||||||
result.skipped += 1;
|
result.skipped += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const src = path.join(folder, file);
|
if (existingCadKeys.has(toCadKey(cadInfo))) {
|
||||||
|
const duplicateTarget = await getDuplicateTarget(file);
|
||||||
|
await fs.copy(fullPath, duplicateTarget.destinationPath, { overwrite: false });
|
||||||
|
|
||||||
|
result.copied += 1;
|
||||||
|
result.duplicates += 1;
|
||||||
|
result.details.push({
|
||||||
|
file,
|
||||||
|
destination: duplicateTarget.destinationDir,
|
||||||
|
reason: 'Duplicato gia presente prima dello smistamento',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const decision = getDestinationDecision(file, config, destinationIndex);
|
const decision = getDestinationDecision(file, config, destinationIndex);
|
||||||
const destDir = decision.destination;
|
const destDir = decision.destination;
|
||||||
|
|
||||||
if (!destDir) {
|
if (!destDir) {
|
||||||
result.skipped += 1;
|
const unroutedTarget = await getUnroutedTarget(file);
|
||||||
result.details.push({ file, reason: decision.reason || 'Nessuna regola trovata' });
|
await fs.copy(fullPath, unroutedTarget.destinationPath, { overwrite: false });
|
||||||
|
|
||||||
|
result.copied += 1;
|
||||||
|
result.unrouted += 1;
|
||||||
|
result.details.push({
|
||||||
|
file,
|
||||||
|
destination: unroutedTarget.destinationDir,
|
||||||
|
reason: decision.reason || 'Nessuna regola trovata',
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dest = path.join(destDir, file);
|
const dest = path.join(destDir, file);
|
||||||
await fs.copy(src, dest, { overwrite: true });
|
await fs.copy(fullPath, dest, { overwrite: true });
|
||||||
|
|
||||||
result.copied += 1;
|
result.copied += 1;
|
||||||
result.details.push({ file, destination: destDir });
|
result.details.push({ file, destination: destDir });
|
||||||
|
|||||||
169
services/unrouted.js
Normal file
169
services/unrouted.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const PRIMARY_UNROUTED_DIR = '/cadroute/__NON_SMISTATI';
|
||||||
|
const HOME_UNROUTED_DIR = path.join(os.homedir(), '.cadroute', '__NON_SMISTATI');
|
||||||
|
const PRIMARY_DUPLICATES_DIR = '/cadroute/duplicati';
|
||||||
|
const HOME_DUPLICATES_DIR = path.join(os.homedir(), '.cadroute', 'duplicati');
|
||||||
|
|
||||||
|
const SPECIAL_TARGETS = {
|
||||||
|
unrouted: {
|
||||||
|
primary: PRIMARY_UNROUTED_DIR,
|
||||||
|
fallback: HOME_UNROUTED_DIR,
|
||||||
|
},
|
||||||
|
duplicates: {
|
||||||
|
primary: PRIMARY_DUPLICATES_DIR,
|
||||||
|
fallback: HOME_DUPLICATES_DIR,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvedDirs = new Map();
|
||||||
|
|
||||||
|
async function resolveTargetDir(kind) {
|
||||||
|
const target = SPECIAL_TARGETS[kind];
|
||||||
|
if (!target) {
|
||||||
|
throw new Error(`Target speciale non supportato: ${kind}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedDirs.has(kind)) {
|
||||||
|
resolvedDirs.set(
|
||||||
|
kind,
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await fs.ensureDir(target.primary);
|
||||||
|
return target.primary;
|
||||||
|
} catch {
|
||||||
|
await fs.ensureDir(target.fallback);
|
||||||
|
return target.fallback;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedDirs.get(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveUnroutedDir() {
|
||||||
|
return resolveTargetDir('unrouted');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveDuplicatesDir() {
|
||||||
|
return resolveTargetDir('duplicates');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUniquePath(destinationDir, fileName) {
|
||||||
|
const parsed = path.parse(fileName);
|
||||||
|
let candidate = path.join(destinationDir, fileName);
|
||||||
|
let counter = 1;
|
||||||
|
|
||||||
|
while (await fs.pathExists(candidate)) {
|
||||||
|
candidate = path.join(destinationDir, `${parsed.name}__${counter}${parsed.ext}`);
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTarget(kind, fileName) {
|
||||||
|
const destinationDir = await resolveTargetDir(kind);
|
||||||
|
const destinationPath = await getUniquePath(destinationDir, fileName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destinationDir,
|
||||||
|
destinationPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUnroutedTarget(fileName) {
|
||||||
|
return getTarget('unrouted', fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDuplicateTarget(fileName) {
|
||||||
|
return getTarget('duplicates', fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listFilesRecursively(rootDir) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
async function walk(currentDir) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(currentDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await walk(fullPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await fs.stat(fullPath).catch(() => null);
|
||||||
|
if (!stats) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({
|
||||||
|
name: entry.name,
|
||||||
|
relativePath: path.relative(rootDir, fullPath),
|
||||||
|
size: stats.size,
|
||||||
|
updatedAt: stats.mtime.toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await walk(rootDir);
|
||||||
|
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath, 'it'));
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listTargetFiles(kind) {
|
||||||
|
const directory = await resolveTargetDir(kind);
|
||||||
|
const files = await listFilesRecursively(directory);
|
||||||
|
return { directory, files };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearTargetFiles(kind) {
|
||||||
|
const directory = await resolveTargetDir(kind);
|
||||||
|
await fs.emptyDir(directory);
|
||||||
|
return { directory };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listUnroutedFiles() {
|
||||||
|
return listTargetFiles('unrouted');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listDuplicateFiles() {
|
||||||
|
return listTargetFiles('duplicates');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearUnroutedFiles() {
|
||||||
|
return clearTargetFiles('unrouted');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearDuplicateFiles() {
|
||||||
|
return clearTargetFiles('duplicates');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getUnroutedTarget,
|
||||||
|
getDuplicateTarget,
|
||||||
|
resolveUnroutedDir,
|
||||||
|
resolveDuplicatesDir,
|
||||||
|
listUnroutedFiles,
|
||||||
|
listDuplicateFiles,
|
||||||
|
clearUnroutedFiles,
|
||||||
|
clearDuplicateFiles,
|
||||||
|
PRIMARY_UNROUTED_DIR,
|
||||||
|
HOME_UNROUTED_DIR,
|
||||||
|
PRIMARY_DUPLICATES_DIR,
|
||||||
|
HOME_DUPLICATES_DIR,
|
||||||
|
};
|
||||||
@@ -2,16 +2,21 @@ const unzipper = require('unzipper');
|
|||||||
const fs = require('fs-extra');
|
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, isCadFile } = require('./router');
|
const { getDestinationDecision, getCadInfo } = require('./router');
|
||||||
const { buildDestinationIndex } = require('./destinationIndex');
|
const { buildDestinationIndex } = require('./destinationIndex');
|
||||||
|
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
|
||||||
|
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
|
||||||
|
|
||||||
async function processZip(zipPath, config) {
|
async function processZip(zipPath, config) {
|
||||||
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 destinationIndex = await buildDestinationIndex(config?.destination);
|
||||||
|
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
|
||||||
const result = {
|
const result = {
|
||||||
scanned: 0,
|
scanned: 0,
|
||||||
copied: 0,
|
copied: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
|
unrouted: 0,
|
||||||
|
duplicates: 0,
|
||||||
details: [],
|
details: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,19 +30,41 @@ async function processZip(zipPath, config) {
|
|||||||
const baseName = path.basename(file);
|
const baseName = path.basename(file);
|
||||||
result.scanned += 1;
|
result.scanned += 1;
|
||||||
|
|
||||||
if (!isCadFile(baseName)) {
|
const cadInfo = getCadInfo(baseName);
|
||||||
|
if (!cadInfo) {
|
||||||
result.skipped += 1;
|
result.skipped += 1;
|
||||||
entry.autodrain();
|
entry.autodrain();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingCadKeys.has(toCadKey(cadInfo))) {
|
||||||
|
const duplicateTarget = await getDuplicateTarget(baseName);
|
||||||
|
await pipeline(entry, fs.createWriteStream(duplicateTarget.destinationPath));
|
||||||
|
|
||||||
|
result.copied += 1;
|
||||||
|
result.duplicates += 1;
|
||||||
|
result.details.push({
|
||||||
|
file: baseName,
|
||||||
|
destination: duplicateTarget.destinationDir,
|
||||||
|
reason: 'Duplicato gia presente prima dello smistamento',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const decision = getDestinationDecision(baseName, config, destinationIndex);
|
const decision = getDestinationDecision(baseName, config, destinationIndex);
|
||||||
const destDir = decision.destination;
|
const destDir = decision.destination;
|
||||||
|
|
||||||
if (!destDir) {
|
if (!destDir) {
|
||||||
result.skipped += 1;
|
const unroutedTarget = await getUnroutedTarget(baseName);
|
||||||
result.details.push({ file: baseName, reason: decision.reason || 'Nessuna regola trovata' });
|
await pipeline(entry, fs.createWriteStream(unroutedTarget.destinationPath));
|
||||||
entry.autodrain();
|
|
||||||
|
result.copied += 1;
|
||||||
|
result.unrouted += 1;
|
||||||
|
result.details.push({
|
||||||
|
file: baseName,
|
||||||
|
destination: unroutedTarget.destinationDir,
|
||||||
|
reason: decision.reason || 'Nessuna regola trovata',
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user