const fs = require('fs/promises'); const path = require('path'); const BOOK_EXT = new Set([ 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'txt', 'md', 'djvu', 'fb2', 'cbz', 'cbr' ]); const DEFAULT_MAX_FILES = 10_000; function pathKey(value) { return process.platform === 'win32' ? value.toLowerCase() : value; } function comparePaths(left, right) { const leftKey = pathKey(left); const rightKey = pathKey(right); if (leftKey < rightKey) return -1; if (leftKey > rightKey) return 1; if (left < right) return -1; if (left > right) return 1; return 0; } async function safeLstat(value) { try { return await fs.lstat(value); } catch (error) { return null; } } async function canonicalEntry(value, expectedType) { const before = await safeLstat(value); if (!before || before.isSymbolicLink() || !before[expectedType]()) return null; let canonical; try { canonical = await fs.realpath(value); } catch (error) { return null; } // Recheck the directory entry after realpath so an entry changed to a link // during discovery is not intentionally traversed or imported. const after = await safeLstat(value); if (!after || after.isSymbolicLink() || !after[expectedType]()) return null; return path.resolve(canonical); } function maximumFrom(options) { if (options && Object.prototype.hasOwnProperty.call(options, 'maxFiles')) { const maximum = options.maxFiles; if (!Number.isSafeInteger(maximum) || maximum < 1) { throw new TypeError('本地导入文件数量上限必须是正整数'); } return maximum; } return DEFAULT_MAX_FILES; } async function discover(paths, options = {}) { const maximum = maximumFrom(options); const selected = Array.isArray(paths) ? paths : [paths]; const candidates = selected .filter((value) => typeof value === 'string' && value.length > 0) .map((value) => path.resolve(value)) .sort(comparePaths); const records = []; const seenFiles = new Set(); const visitedDirectories = new Set(); function addFile(canonical) { const key = pathKey(canonical); if (seenFiles.has(key)) return; if (records.length >= maximum) { throw new Error(`本地导入文件数量超过上限(最多 ${maximum} 个)`); } seenFiles.add(key); const name = path.basename(canonical); records.push({ path: canonical, name, format: path.extname(name).slice(1).toLowerCase(), parentName: path.basename(path.dirname(canonical)) }); } async function visitFile(value) { const extension = path.extname(value).slice(1).toLowerCase(); if (!BOOK_EXT.has(extension)) return; const canonical = await canonicalEntry(value, 'isFile'); if (canonical) addFile(canonical); } async function visitDirectory(value) { const canonical = await canonicalEntry(value, 'isDirectory'); if (!canonical) return; const key = pathKey(canonical); if (visitedDirectories.has(key)) return; visitedDirectories.add(key); let entries; try { entries = await fs.readdir(canonical, { withFileTypes: true }); } catch (error) { return; } entries.sort((left, right) => comparePaths(left.name, right.name)); for (const entry of entries) { const child = path.join(canonical, entry.name); const stat = await safeLstat(child); if (!stat || stat.isSymbolicLink()) continue; if (stat.isDirectory()) { await visitDirectory(child); } else if (stat.isFile()) { await visitFile(child); } } } for (const candidate of candidates) { const stat = await safeLstat(candidate); if (!stat || stat.isSymbolicLink()) continue; if (stat.isDirectory()) { await visitDirectory(candidate); } else if (stat.isFile()) { await visitFile(candidate); } } records.sort((left, right) => comparePaths(left.path, right.path)); return records; } module.exports = { discover };