feat(router): rilevamento duplicati pre-smistamento e instradamento in duplicati

This commit is contained in:
2026-03-16 10:27:12 +01:00
parent 3a567c390c
commit 5e4e03a103
4 changed files with 180 additions and 23 deletions

View 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 };

View File

@@ -1,8 +1,9 @@
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 { getUnroutedTarget } = require('./unrouted'); const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
async function collectFilesRecursively(rootDir) { async function collectFilesRecursively(rootDir) {
const files = []; const files = [];
@@ -31,11 +32,13 @@ async function collectFilesRecursively(rootDir) {
async function processFolder(folder, config) { async function processFolder(folder, config) {
const files = await collectFilesRecursively(folder); 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, unrouted: 0,
duplicates: 0,
details: [], details: [],
}; };
@@ -43,11 +46,26 @@ async function processFolder(folder, config) {
const file = fileName; const file = fileName;
result.scanned += 1; result.scanned += 1;
if (!isCadFile(file)) { const cadInfo = getCadInfo(file);
if (!cadInfo) {
result.skipped += 1; result.skipped += 1;
continue; 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 decision = getDestinationDecision(file, config, destinationIndex);
const destDir = decision.destination; const destDir = decision.destination;

View File

@@ -4,23 +4,52 @@ const os = require('os');
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 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,
},
};
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() { async function resolveUnroutedDir() {
if (!resolvedUnroutedDirPromise) { return resolveTargetDir('unrouted');
resolvedUnroutedDirPromise = (async () => {
try {
await fs.ensureDir(PRIMARY_UNROUTED_DIR);
return PRIMARY_UNROUTED_DIR;
} catch {
await fs.ensureDir(HOME_UNROUTED_DIR);
return HOME_UNROUTED_DIR;
}
})();
} }
return resolvedUnroutedDirPromise; async function resolveDuplicatesDir() {
return resolveTargetDir('duplicates');
} }
async function getUniquePath(destinationDir, fileName) { async function getUniquePath(destinationDir, fileName) {
@@ -36,8 +65,8 @@ async function getUniquePath(destinationDir, fileName) {
return candidate; return candidate;
} }
async function getUnroutedTarget(fileName) { async function getTarget(kind, fileName) {
const destinationDir = await resolveUnroutedDir(); const destinationDir = await resolveTargetDir(kind);
const destinationPath = await getUniquePath(destinationDir, fileName); const destinationPath = await getUniquePath(destinationDir, fileName);
return { 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) { async function listFilesRecursively(rootDir) {
const files = []; const files = [];
@@ -88,16 +125,45 @@ async function listFilesRecursively(rootDir) {
return files; return files;
} }
async function listUnroutedFiles() { async function listTargetFiles(kind) {
const directory = await resolveUnroutedDir(); const directory = await resolveTargetDir(kind);
const files = await listFilesRecursively(directory); const files = await listFilesRecursively(directory);
return { directory, files }; 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 = { module.exports = {
getUnroutedTarget, getUnroutedTarget,
getDuplicateTarget,
resolveUnroutedDir, resolveUnroutedDir,
resolveDuplicatesDir,
listUnroutedFiles, listUnroutedFiles,
listDuplicateFiles,
clearUnroutedFiles,
clearDuplicateFiles,
PRIMARY_UNROUTED_DIR, PRIMARY_UNROUTED_DIR,
HOME_UNROUTED_DIR, HOME_UNROUTED_DIR,
PRIMARY_DUPLICATES_DIR,
HOME_DUPLICATES_DIR,
}; };

View File

@@ -2,18 +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 { getUnroutedTarget } = require('./unrouted'); 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, unrouted: 0,
duplicates: 0,
details: [], details: [],
}; };
@@ -27,12 +30,27 @@ 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;