lib/storage)The lib/storage module is the single source of truth for reading delivery note metadata from the NAS share.
All code that needs to list branches/years/months/days/files on the NAS should go through this module instead of using Node.js fs directly. This keeps filesystem listing logic centralized and makes it easier to change conventions later.
Note:
- Binary streaming endpoints (e.g. PDF streaming) may use direct filesystem streaming APIs (
fs.createReadStream) for performance and memory safety.- Such endpoints must still follow the same safety rules (validated segments, no path traversal) and the same error mapping conventions (
lib/api/storageErrors.js).
Resolve paths under the NAS root (NAS_ROOT_PATH).
Provide intention-revealing helpers:
listBranches() → ['NL01', 'NL02', ...]listYears(branch) → ['2023', '2024', ...]listMonths(branch, year) → ['01', '02', ...]listDays(branch, year, month) → ['01', '02', ...]listFiles(branch, year, month, day) → [{ name, relativePath }, ...]Enforce read-only behavior.
Use async filesystem APIs (fs/promises).
NAS_ROOT_PATH (required)The module depends on:
NAS_ROOT_PATH — absolute Unix path where the NAS share is mounted inside the app container.Default/typical value:
NAS_ROOT_PATH=/mnt/niederlassungen
Important:
lib/storage reads process.env.NAS_ROOT_PATH on demand and does not cache it at module load.NAS_ROOT_PATH is missing, lib/storage throws (fail fast).The application code always expects the NAS path inside the container to be:
/mnt/niederlassungenWhich host folder is mounted there is an environment concern:
Server (docker-compose.yml) mounts the real NAS:
volumes:
- /mnt/niederlassungen:/mnt/niederlassungen:ro
Local development (docker-compose.local.yml) mounts a local fixture folder:
volumes:
- ./.local_nas:/mnt/niederlassungen:ro
This separation keeps code identical across environments while allowing safe local testing.
lib/storage assumes the following structure under NAS_ROOT_PATH:
NAS_ROOT_PATH/
@Recently-Snapshot/ # ignored
NL01/
2024/
10/
23/
file1.pdf
file2.pdf
...
Rules:
NL<Number> (e.g. NL01).2024)..pdf files are returned by listFiles().lib/storage does not swallow errors:
fs.promises.readdir throws.lib/storage remains intentionally small and focused on filesystem reads.API routes map filesystem errors into standardized HTTP responses:
If a requested path does not exist (e.g. ENOENT) and the NAS root is accessible:
404 with FS_NOT_FOUNDIf the NAS root itself is missing/unreachable or other unexpected filesystem errors occur:
500 with FS_STORAGE_ERRORThis mapping is implemented in lib/api/storageErrors.js and used by route handlers.
The NAS content can change at any time (new scans). To reduce filesystem load while keeping freshness predictable, lib/storage implements a small process-local TTL micro-cache.
listBranches() / listYears() → 60 secondslistMonths() / listDays() / listFiles() → 15 secondsTTL is a maximum staleness guarantee.
Cache is process-local.
Cache keys include NAS_ROOT_PATH.
The storage module currently focuses on listing directory contents.
For endpoints that must return binary file data (PDF streaming/download), a direct stream approach is preferred:
fs.stat() first (for existence/type) and then fs.createReadStream().A streaming endpoint must never accept arbitrary paths.
Rules:
Build the absolute file path from:
NAS_ROOT_PATHbranch, year, month, day)filename)Validate route segments using strict patterns:
branch: ^NL\d+$year: ^\d{4}$month: 01–12day: 01–31Validate filename:
/, \, or .. segments).pdf is allowedApply a root containment check:
NAS_ROOT_PATHEven when the happy-path response is binary, errors must remain standardized JSON.
Recommended approach:
stat(absPath)
If it throws:
mapStorageReadError(err, { details })If stat succeeds but !stat.isFile():
404 FS_NOT_FOUNDFor PDF streaming:
Content-Type: application/pdfContent-Disposition: inline; filename="..." (default)Content-Disposition: attachment; filename="..." (when download=1)Cache-Control: no-storePotential follow-ups for the storage layer:
A dedicated helper for streaming files (e.g. openPdfStream(...)) that centralizes:
stat() + stream creationThis is optional; the current v1 design keeps lib/storage focused on listing operations.