From 5e4e03a103af8814d4a5786eaca1b5321a4e3fd9 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 16 Mar 2026 10:27:12 +0100 Subject: [PATCH] feat(router): rilevamento duplicati pre-smistamento e instradamento in duplicati --- services/duplicateIndex.js | 55 ++++++++++++++++++++ services/folderProcessor.js | 24 +++++++-- services/unrouted.js | 100 ++++++++++++++++++++++++++++++------ services/zipProcessor.js | 24 +++++++-- 4 files changed, 180 insertions(+), 23 deletions(-) create mode 100644 services/duplicateIndex.js diff --git a/services/duplicateIndex.js b/services/duplicateIndex.js new file mode 100644 index 0000000..4ad10b6 --- /dev/null +++ b/services/duplicateIndex.js @@ -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 }; diff --git a/services/folderProcessor.js b/services/folderProcessor.js index 12be5b5..b0c73a9 100644 --- a/services/folderProcessor.js +++ b/services/folderProcessor.js @@ -1,8 +1,9 @@ const fs = require('fs-extra'); const path = require('path'); -const { getDestinationDecision, isCadFile } = require('./router'); +const { getDestinationDecision, getCadInfo } = require('./router'); const { buildDestinationIndex } = require('./destinationIndex'); -const { getUnroutedTarget } = require('./unrouted'); +const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex'); +const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted'); async function collectFilesRecursively(rootDir) { const files = []; @@ -31,11 +32,13 @@ async function collectFilesRecursively(rootDir) { async function processFolder(folder, config) { 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: [], }; @@ -43,11 +46,26 @@ async function processFolder(folder, config) { const file = fileName; result.scanned += 1; - if (!isCadFile(file)) { + const cadInfo = getCadInfo(file); + if (!cadInfo) { result.skipped += 1; continue; } + 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; diff --git a/services/unrouted.js b/services/unrouted.js index cab4645..61d05e0 100644 --- a/services/unrouted.js +++ b/services/unrouted.js @@ -4,23 +4,52 @@ 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'); -let resolvedUnroutedDirPromise = null; +const SPECIAL_TARGETS = { + unrouted: { + primary: PRIMARY_UNROUTED_DIR, + fallback: HOME_UNROUTED_DIR, + }, + duplicates: { + primary: PRIMARY_DUPLICATES_DIR, + fallback: HOME_DUPLICATES_DIR, + }, +}; -async function resolveUnroutedDir() { - if (!resolvedUnroutedDirPromise) { - resolvedUnroutedDirPromise = (async () => { - try { - await fs.ensureDir(PRIMARY_UNROUTED_DIR); - return PRIMARY_UNROUTED_DIR; - } catch { - await fs.ensureDir(HOME_UNROUTED_DIR); - return HOME_UNROUTED_DIR; - } - })(); +const resolvedDirs = new Map(); + +async function resolveTargetDir(kind) { + const target = SPECIAL_TARGETS[kind]; + if (!target) { + throw new Error(`Target speciale non supportato: ${kind}`); } - return resolvedUnroutedDirPromise; + 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) { @@ -36,8 +65,8 @@ async function getUniquePath(destinationDir, fileName) { return candidate; } -async function getUnroutedTarget(fileName) { - const destinationDir = await resolveUnroutedDir(); +async function getTarget(kind, fileName) { + const destinationDir = await resolveTargetDir(kind); const destinationPath = await getUniquePath(destinationDir, fileName); return { @@ -46,6 +75,14 @@ async function getUnroutedTarget(fileName) { }; } +async function getUnroutedTarget(fileName) { + return getTarget('unrouted', fileName); +} + +async function getDuplicateTarget(fileName) { + return getTarget('duplicates', fileName); +} + async function listFilesRecursively(rootDir) { const files = []; @@ -88,16 +125,45 @@ async function listFilesRecursively(rootDir) { return files; } -async function listUnroutedFiles() { - const directory = await resolveUnroutedDir(); +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, }; diff --git a/services/zipProcessor.js b/services/zipProcessor.js index 0e23d08..09d9aa8 100644 --- a/services/zipProcessor.js +++ b/services/zipProcessor.js @@ -2,18 +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 { getUnroutedTarget } = require('./unrouted'); +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: [], }; @@ -27,12 +30,27 @@ 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;