Files
Multi-Hoster-Upload/lib/import-preflight.js
T
Sucukdeluxe 3da1b08514 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.
2026-08-16 22:01:16 +02:00

186 lines
6.8 KiB
JavaScript

(function initImportPreflight(root, factory) {
const api = typeof module === 'object' && module.exports
? factory(require('path'), require('./filename-filter'))
: factory({
normalize: value => String(value).replace(/[\\/]+/g, '/'),
basename: value => String(value).split(/[\\/]/).pop() || ''
}, root.FilenameFilter);
if (typeof module === 'object' && module.exports) module.exports = api;
if (root) root.ImportPreflight = api;
})(typeof window !== 'undefined' ? window : globalThis, function createImportPreflight(path, filenameFilter) {
const { applyFilenameFilter } = filenameFilter;
function normalizePathValue(value) {
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) {
const source = value && typeof value === 'object' ? value : {};
const filePath = normalizePathValue(typeof value === 'string' ? value : source.path);
const sourceName = typeof value === 'string' ? '' : String(source.name ?? '').trim();
return {
path: filePath,
name: sourceName || path.basename(filePath),
size: Number.isFinite(Number(source.size)) ? Number(source.size) : null
};
}
function createPathKey(value, caseInsensitive) {
const normalized = normalizePathValue(value);
return caseInsensitive ? normalized.toLocaleLowerCase('en-US') : normalized;
}
async function mapWithConcurrency(items, concurrency, operation) {
const results = new Array(items.length);
let cursor = 0;
async function worker() {
while (cursor < items.length) {
const index = cursor++;
results[index] = await operation(items[index], index);
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
return results;
}
function unavailableReason(result) {
if (!result || result.exists === false) return 'missing';
if (result.readable === false) return 'unreadable';
const size = Number(result.size);
if (!Number.isFinite(size) || size <= 0) return 'empty';
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);
const existing = new Set((Array.isArray(options.existingPaths) ? options.existingPaths : [])
.map(value => createPathKey(value && typeof value === 'object' ? value.path : value, caseInsensitive))
.filter(Boolean));
const duplicates = [];
const unavailable = [];
const unique = [];
for (const value of input) {
const entry = normalizeEntry(value);
if (!entry.path) {
unavailable.push({ ...entry, reason: 'missing' });
continue;
}
const key = createPathKey(entry.path, caseInsensitive);
if (existing.has(key)) {
duplicates.push(entry);
continue;
}
existing.add(key);
unique.push(entry);
}
const filtered = applyFilenameFilter(unique, options.filenameFilter);
const concurrency = Math.max(1, Math.min(32, Math.trunc(Number(options.concurrency)) || 8));
const inspectPath = typeof options.inspectPath === 'function'
? options.inspectPath
: async (_entryPath, entry) => ({ exists: true, readable: true, size: entry.size });
const inspected = await mapWithConcurrency(filtered.accepted, concurrency, async entry => {
try {
const result = await inspectPath(entry.path, entry);
const reason = unavailableReason(result);
if (reason) return { entry: { ...entry, size: Number(result?.size) || 0 }, reason };
return { entry: { ...entry, size: Number(result.size) }, reason: '' };
} catch (error) {
return { entry, reason: error && error.code === 'ENOENT' ? 'missing' : 'unreadable' };
}
});
const accepted = [];
for (const result of inspected) {
if (result.reason) unavailable.push({ ...result.entry, reason: result.reason });
else accepted.push(result.entry);
}
return {
candidateCount: input.length,
duplicateCount: duplicates.length,
filteredCount: filtered.excluded.length,
unavailableCount: unavailable.length,
acceptedCount: accepted.length,
accepted,
duplicates,
filtered: filtered.excluded,
unavailable
};
}
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 = normalizeSelectedHosters(input.selectedHosters);
const settings = input.hosterSettings && typeof input.hosterSettings === 'object' ? input.hosterSettings : {};
let jobCount = 0;
for (const file of accepted) {
jobCount += getEligibleImportHosters(file, selectedHosters, settings).length;
}
const sizeLimitedJobCount = accepted.length * selectedHosters.length - jobCount;
return {
candidateCount: Number(inspection.candidateCount) || 0,
duplicateCount: Number(inspection.duplicateCount) || 0,
filteredCount: Number(inspection.filteredCount) || 0,
unavailableCount: Number(inspection.unavailableCount) || 0,
acceptedCount: accepted.length,
targetCount: selectedHosters.length,
jobCount,
sizeLimitedJobCount
};
}
return {
getEligibleImportHosters,
inspectImportEntries,
inspectReadableImportPath,
isImportPairEligible,
summarizeImportPlan
};
});