fix: harden import preflight admission

Keep configured size-limit counts identical to real queue admission, invalidate cancelled import generations, preserve explicit empty destination choices, normalize Windows namespace aliases, inspect files through one read handle, and retain exact rejection balances.
This commit is contained in:
Sucukdeluxe
2026-08-16 22:01:16 +02:00
parent 88a3ee0937
commit 3da1b08514
5 changed files with 190 additions and 49 deletions
+52 -11
View File
@@ -11,8 +11,15 @@
const { applyFilenameFilter } = filenameFilter;
function normalizePathValue(value) {
const text = String(value ?? '').trim();
return text ? path.normalize(text) : '';
let text = String(value ?? '').trim();
if (!text) return '';
const uncNamespace = text.match(/^[\\/]{2}\?[\\/]UNC[\\/]/i);
if (uncNamespace) text = `\\\\${text.slice(uncNamespace[0].length)}`;
else {
const driveNamespace = text.match(/^[\\/]{2}\?[\\/](?=[A-Za-z]:[\\/])/);
if (driveNamespace) text = text.slice(driveNamespace[0].length);
}
return path.normalize(text);
}
function normalizeEntry(value) {
@@ -52,6 +59,25 @@ function unavailableReason(result) {
return '';
}
async function inspectReadableImportPath(filePath, openPath) {
let fileHandle = null;
try {
fileHandle = await openPath(filePath, 'r');
const fileStat = await fileHandle.stat();
if (!fileStat.isFile()) return { exists: true, readable: false, size: fileStat.size };
return { exists: true, readable: true, size: fileStat.size };
} catch (error) {
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return { exists: false };
return { exists: true, readable: false };
} finally {
if (fileHandle) {
try {
await fileHandle.close();
} catch {}
}
}
}
async function inspectImportEntries(entries, options = {}) {
const input = Array.isArray(entries) ? entries : [];
const caseInsensitive = options.caseInsensitive ?? (typeof process === 'object' ? process.platform === 'win32' : true);
@@ -111,20 +137,32 @@ async function inspectImportEntries(entries, options = {}) {
};
}
function normalizeSelectedHosters(values) {
return Array.from(new Set((Array.isArray(values) ? values : [])
.map(value => String(value ?? '').trim())
.filter(Boolean)));
}
function isImportPairEligible(file, hoster, hosterSettings = {}) {
const maxSizeMb = Number(hosterSettings?.[hoster]?.maxSizeMb);
return !(maxSizeMb > 0 && Number(file?.size) > maxSizeMb * 1024 * 1024);
}
function getEligibleImportHosters(file, selectedHosters, hosterSettings = {}) {
return normalizeSelectedHosters(selectedHosters)
.filter(hoster => isImportPairEligible(file, hoster, hosterSettings));
}
function summarizeImportPlan(input = {}) {
const inspection = input.inspection && typeof input.inspection === 'object' ? input.inspection : {};
const accepted = Array.isArray(inspection.accepted) ? inspection.accepted : [];
const selectedHosters = Array.from(new Set((Array.isArray(input.selectedHosters) ? input.selectedHosters : [])
.map(value => String(value ?? '').trim())
.filter(Boolean)));
const selectedHosters = normalizeSelectedHosters(input.selectedHosters);
const settings = input.hosterSettings && typeof input.hosterSettings === 'object' ? input.hosterSettings : {};
let sizeLimitedJobCount = 0;
let jobCount = 0;
for (const file of accepted) {
for (const hoster of selectedHosters) {
const maxSizeMb = Number(settings[hoster]?.maxSizeMb);
if (maxSizeMb > 0 && Number(file.size) > maxSizeMb * 1024 * 1024) sizeLimitedJobCount++;
}
jobCount += getEligibleImportHosters(file, selectedHosters, settings).length;
}
const sizeLimitedJobCount = accepted.length * selectedHosters.length - jobCount;
return {
candidateCount: Number(inspection.candidateCount) || 0,
duplicateCount: Number(inspection.duplicateCount) || 0,
@@ -132,13 +170,16 @@ function summarizeImportPlan(input = {}) {
unavailableCount: Number(inspection.unavailableCount) || 0,
acceptedCount: accepted.length,
targetCount: selectedHosters.length,
jobCount: accepted.length * selectedHosters.length - sizeLimitedJobCount,
jobCount,
sizeLimitedJobCount
};
}
return {
getEligibleImportHosters,
inspectImportEntries,
inspectReadableImportPath,
isImportPairEligible,
summarizeImportPlan
};
});