// lib/storage.js // ----------------------------------------------------------------------------- // Central abstraction layer for reading files and directories from the NAS // share mounted at `NAS_ROOT_PATH` (e.g. `/mnt/niederlassungen`). // // All access to the branch/year/month/day/PDF structure should go through // these functions instead of using `fs` directly in route handlers. // // - Read-only: no write/delete operations here. // - Async only: uses `fs/promises` + async/await to avoid blocking the event loop. // ----------------------------------------------------------------------------- import fs from "node:fs/promises"; // Promise-based filesystem API import path from "node:path"; // Safe path utilities (handles separators) // Root directory of the NAS share, injected via environment variable. // On the Linux app server, this is typically `/mnt/niederlassungen`. // Do NOT cache process.env.NAS_ROOT_PATH at module load time. // Instead, resolve it on demand so tests (and runtime) can change it. function getRoot() { const root = process.env.NAS_ROOT_PATH; if (!root) { throw new Error("NAS_ROOT_PATH environment variable is not set"); } return root; } // Build an absolute path below the NAS root from a list of segments. function fullPath(...segments) { const root = getRoot(); return path.join(root, ...segments.map(String)); } // Compare strings that represent numbers in a numeric way. // This ensures "2" comes before "10" (2 < 10), not after. function sortNumericStrings(a, b) { const na = parseInt(a, 10); const nb = parseInt(b, 10); if (!Number.isNaN(na) && !Number.isNaN(nb)) { return na - nb; } // Fallback to localeCompare if parsing fails return a.localeCompare(b, "en"); } // ----------------------------------------------------------------------------- // 1. Branches (NL01, NL02, ...) // Path pattern: `${ROOT}/NLxx` // ----------------------------------------------------------------------------- export async function listBranches() { // Read the root directory of the NAS share. // `withFileTypes: true` returns `Dirent` objects so we can call `isDirectory()` // without extra stat() calls, which is more efficient. const entries = await fs.readdir(fullPath(), { withFileTypes: true }); return ( entries .filter( (entry) => entry.isDirectory() && // only directories entry.name !== "@Recently-Snapshot" && // ignore QNAP snapshot folder /^NL\d+$/i.test(entry.name) // keep only names like "NL01", "NL02", ... ) .map((entry) => entry.name) // Sort by numeric branch number: NL1, NL2, ..., NL10 .sort((a, b) => sortNumericStrings(a.replace("NL", ""), b.replace("NL", "")) ) ); } // ----------------------------------------------------------------------------- // 2. Years (2023, 2024, ...) // Path pattern: `${ROOT}/${branch}/${year}` // ----------------------------------------------------------------------------- export async function listYears(branch) { const dir = fullPath(branch); const entries = await fs.readdir(dir, { withFileTypes: true }); return entries .filter( (entry) => entry.isDirectory() && /^\d{4}$/.test(entry.name) // exactly 4 digits → year folders like "2024" ) .map((entry) => entry.name) .sort(sortNumericStrings); } // ----------------------------------------------------------------------------- // 3. Months (01–12) // Path pattern: `${ROOT}/${branch}/${year}/${month}` // ----------------------------------------------------------------------------- export async function listMonths(branch, year) { const dir = fullPath(branch, year); const entries = await fs.readdir(dir, { withFileTypes: true }); return ( entries .filter( (entry) => entry.isDirectory() && /^\d{1,2}$/.test(entry.name) // supports "1" or "10", we normalize below ) // Normalize to two digits so the UI shows "01", "02", ..., "12" .map((entry) => entry.name.trim().padStart(2, "0")) .sort(sortNumericStrings) ); } // ----------------------------------------------------------------------------- // 4. Days (01–31) // Path pattern: `${ROOT}/${branch}/${year}/${month}/${day}` // ----------------------------------------------------------------------------- export async function listDays(branch, year, month) { const dir = fullPath(branch, year, month); const entries = await fs.readdir(dir, { withFileTypes: true }); return entries .filter( (entry) => entry.isDirectory() && /^\d{1,2}$/.test(entry.name) // supports "1" or "23" ) .map((entry) => entry.name.trim().padStart(2, "0")) .sort(sortNumericStrings); } // ----------------------------------------------------------------------------- // 5. Files (PDFs) for a given day // Path pattern: `${ROOT}/${branch}/${year}/${month}/${day}/.pdf` // ----------------------------------------------------------------------------- export async function listFiles(branch, year, month, day) { const dir = fullPath(branch, year, month, day); const entries = await fs.readdir(dir); return ( entries // We only care about PDF files at the moment .filter((name) => name.toLowerCase().endsWith(".pdf")) .sort((a, b) => a.localeCompare(b, "en")) .map((name) => ({ // Just the file name, e.g. "Stapel-1_Seiten-1_Zeit-1048.pdf" name, // Relative path from the NAS root, used for download URLs etc. // Example: "NL01/2024/10/23/Stapel-1_Seiten-1_Zeit-1048.pdf" relativePath: `${branch}/${year}/${month}/${day}/${name}`, })) ); }