| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- // 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}/<file>.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}`,
- }))
- );
- }
|