import { constants } from 'fs'; import * as fs from 'fs/promises'; import * as path from 'path'; import { pipeline } from 'stream/promises'; /** * Macht aus einem (teils von außen stammenden) Namen genau ein Pfadsegment: * Pfadtrenner und Steuerzeichen werden zu `_`, `.`/`..`/leer zu `_`. * Verhindert, dass z.B. ein Barcode-Wert den Exportpfad verlässt. */ export function sichererDateiname(name: string): string { // eslint-disable-next-line no-control-regex const bereinigt = name.replace(/[/\\\u0000-\u001f\u007f]/g, '_'); return bereinigt === '' || bereinigt === '.' || bereinigt === '..' ? '_' : bereinigt; } /** * Ersetzt `{Name}`-Platzhalter in einer URL-Vorlage URL-kodiert, damit ein * Wert Pfad, Query oder Fragment der konfigurierten URL nicht ändern kann. */ export function ersetzeUrlPlatzhalter( vorlage: string, werte: Record, ): string { return Object.entries(werte).reduce( (url, [name, wert]) => url.split(`{${name}}`).join(encodeURIComponent(wert)), vorlage, ); } /** * Verschiebt eine Datei von einer fremd beschreibbaren Freigabe (Scanner), * aber nur, wenn sie eine reguläre Datei in einem echten Verzeichnis ist. * Symlinks würden sonst beliebige Dateien des Servers in den Posteingang * holen – beim Kopieren über Mount-Grenzen folgt `copyFile` ihnen. */ export async function uebernehmeRegulaereDatei( quelle: string, ziel: string, optionen: { rename?: (von: string, nach: string) => Promise; } = {}, ): Promise { const verzeichnis = await fs.lstat(path.dirname(quelle)); if (!verzeichnis.isDirectory()) { throw new Error(`${path.dirname(quelle)} ist kein echtes Verzeichnis`); } if (!(await fs.lstat(quelle)).isFile()) { throw new Error(`${quelle} ist keine reguläre Datei`); } const rename = optionen.rename ?? fs.rename; try { await rename(quelle, ziel); // Zwischen Prüfung und rename könnte die Datei ersetzt worden sein. if (!(await fs.lstat(ziel)).isFile()) { await fs.unlink(ziel); throw new Error(`${quelle} ist keine reguläre Datei`); } return; } catch (err) { if ((err as NodeJS.ErrnoException).code !== 'EXDEV') throw err; } // Über Mount-Grenzen: ohne Symlinks zu folgen öffnen, dann kopieren. const lesen = await fs.open( quelle, constants.O_RDONLY | constants.O_NOFOLLOW, ); try { if (!(await lesen.stat()).isFile()) { throw new Error(`${quelle} ist keine reguläre Datei`); } const schreiben = await fs.open(ziel, 'wx'); try { await pipeline(lesen.createReadStream(), schreiben.createWriteStream()); } catch (err) { await fs.unlink(ziel).catch(() => undefined); throw err; } finally { await schreiben.close().catch(() => undefined); } } finally { await lesen.close().catch(() => undefined); } // Wenn das Löschen scheitert, Kopie zurückrollen, damit ein kaputter Mount // nicht bei jedem Neustart Duplikate produziert. try { await fs.unlink(quelle); } catch (err) { await fs.unlink(ziel).catch(() => undefined); throw err; } }