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 fs = require('fs-extra');
|
||||
|
||||
const { processFolder } = require('./services/folderProcessor');
|
||||
const { processZip } = require('./services/zipProcessor');
|
||||
const {
|
||||
resolveUnroutedDir,
|
||||
listUnroutedFiles,
|
||||
listDuplicateFiles,
|
||||
clearUnroutedFiles,
|
||||
clearDuplicateFiles,
|
||||
} = require('./services/unrouted');
|
||||
|
||||
const CAD_EXTENSIONS = ['prt', 'asm', 'dwr'];
|
||||
const DEFAULT_DESTINATION = './output/cad';
|
||||
const SETTINGS_FILENAME = 'cad-router-settings.json';
|
||||
const LINUX_RUNTIME_DIR = '.cadroute';
|
||||
|
||||
function buildConfig(destination) {
|
||||
const resolvedDestination = String(destination || '').trim() || DEFAULT_DESTINATION;
|
||||
@@ -18,7 +26,19 @@ function buildConfig(destination) {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -59,6 +79,7 @@ function createWindow() {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
await ensureRuntimeDirectory();
|
||||
const persistedDestination = await loadPersistedDestination();
|
||||
config = buildConfig(persistedDestination);
|
||||
createWindow();
|
||||
@@ -154,3 +175,19 @@ ipcMain.handle('update-destination', async (_event, payload) => {
|
||||
await persistDestination(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'),
|
||||
selectDestinationFolder: () => ipcRenderer.invoke('select-destination-folder'),
|
||||
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;
|
||||
}
|
||||
|
||||
.rule-row.preview-actions {
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.rule-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
@@ -161,6 +166,75 @@
|
||||
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) {
|
||||
.rule-row {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -200,11 +274,29 @@
|
||||
<button id="browseDestinationBtn" class="browse">Sfoglia</button>
|
||||
<button id="saveDestinationBtn">Salva</button>
|
||||
</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>
|
||||
</section>
|
||||
<pre id="output">Pronto.</pre>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,9 +5,18 @@ const output = document.getElementById('output');
|
||||
const destinationInput = document.getElementById('destinationInput');
|
||||
const browseDestinationBtn = document.getElementById('browseDestinationBtn');
|
||||
const saveDestinationBtn = document.getElementById('saveDestinationBtn');
|
||||
const openUnroutedBtn = document.getElementById('openUnroutedBtn');
|
||||
const openDuplicatesBtn = document.getElementById('openDuplicatesBtn');
|
||||
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';
|
||||
let isProcessing = false;
|
||||
let currentPreviewKind = null;
|
||||
|
||||
function setLoading(isLoading) {
|
||||
folderBtn.disabled = isLoading;
|
||||
@@ -18,6 +27,52 @@ function setDestinationLoading(isLoading) {
|
||||
destinationInput.disabled = isLoading;
|
||||
browseDestinationBtn.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) {
|
||||
@@ -33,6 +88,8 @@ function renderResult(title, result) {
|
||||
`scansionati: ${result.scanned ?? 0}`,
|
||||
`copiati: ${result.copied ?? 0}`,
|
||||
`saltati: ${result.skipped ?? 0}`,
|
||||
`non smistati: ${result.unrouted ?? 0}`,
|
||||
`duplicati: ${result.duplicates ?? 0}`,
|
||||
'',
|
||||
'dettagli (max 20):',
|
||||
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() {
|
||||
try {
|
||||
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 path = require('path');
|
||||
const { getDestinationDecision, isCadFile } = require('./router');
|
||||
const { getDestinationDecision, getCadInfo } = require('./router');
|
||||
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) {
|
||||
const entries = await fs.readdir(folder, { withFileTypes: true });
|
||||
const files = await collectFilesRecursively(folder);
|
||||
const destinationIndex = await buildDestinationIndex(config?.destination);
|
||||
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
|
||||
const result = {
|
||||
scanned: 0,
|
||||
copied: 0,
|
||||
skipped: 0,
|
||||
unrouted: 0,
|
||||
duplicates: 0,
|
||||
details: [],
|
||||
};
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const file = entry.name;
|
||||
for (const { fullPath, fileName } of files) {
|
||||
const file = fileName;
|
||||
result.scanned += 1;
|
||||
|
||||
if (!isCadFile(file)) {
|
||||
const cadInfo = getCadInfo(file);
|
||||
if (!cadInfo) {
|
||||
result.skipped += 1;
|
||||
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 destDir = decision.destination;
|
||||
|
||||
if (!destDir) {
|
||||
result.skipped += 1;
|
||||
result.details.push({ file, reason: decision.reason || 'Nessuna regola trovata' });
|
||||
const unroutedTarget = await getUnroutedTarget(file);
|
||||
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;
|
||||
}
|
||||
|
||||
const dest = path.join(destDir, file);
|
||||
await fs.copy(src, dest, { overwrite: true });
|
||||
await fs.copy(fullPath, dest, { overwrite: true });
|
||||
|
||||
result.copied += 1;
|
||||
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 path = require('path');
|
||||
const { pipeline } = require('stream/promises');
|
||||
const { getDestinationDecision, isCadFile } = require('./router');
|
||||
const { getDestinationDecision, getCadInfo } = require('./router');
|
||||
const { buildDestinationIndex } = require('./destinationIndex');
|
||||
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
|
||||
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
|
||||
|
||||
async function processZip(zipPath, config) {
|
||||
const stream = fs.createReadStream(zipPath).pipe(unzipper.Parse({ forceStream: true }));
|
||||
const destinationIndex = await buildDestinationIndex(config?.destination);
|
||||
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
|
||||
const result = {
|
||||
scanned: 0,
|
||||
copied: 0,
|
||||
skipped: 0,
|
||||
unrouted: 0,
|
||||
duplicates: 0,
|
||||
details: [],
|
||||
};
|
||||
|
||||
@@ -25,19 +30,41 @@ async function processZip(zipPath, config) {
|
||||
const baseName = path.basename(file);
|
||||
result.scanned += 1;
|
||||
|
||||
if (!isCadFile(baseName)) {
|
||||
const cadInfo = getCadInfo(baseName);
|
||||
if (!cadInfo) {
|
||||
result.skipped += 1;
|
||||
entry.autodrain();
|
||||
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 destDir = decision.destination;
|
||||
|
||||
if (!destDir) {
|
||||
result.skipped += 1;
|
||||
result.details.push({ file: baseName, reason: decision.reason || 'Nessuna regola trovata' });
|
||||
entry.autodrain();
|
||||
const unroutedTarget = await getUnroutedTarget(baseName);
|
||||
await pipeline(entry, fs.createWriteStream(unroutedTarget.destinationPath));
|
||||
|
||||
result.copied += 1;
|
||||
result.unrouted += 1;
|
||||
result.details.push({
|
||||
file: baseName,
|
||||
destination: unroutedTarget.destinationDir,
|
||||
reason: decision.reason || 'Nessuna regola trovata',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user