- Scanner: Dateien von der Scan-Freigabe werden nur übernommen, wenn sie
reguläre Dateien in echten Verzeichnissen sind. chokidar folgt keinen
Symlinks mehr; beim Kopieren über Mount-Grenzen (/mnt/scans → /mnt/data)
wird mit O_NOFOLLOW geöffnet und nur exklusiv geschrieben. Bisher holte
ein Symlink wie scan.pdf -> /proc/self/environ die Zieldatei in den
Posteingang.
- Export: Dateinamen (enthalten u.a. Barcode-Werte) werden auf ein
Pfadsegment reduziert – „../“ verlässt den FTP-Zielordner nicht mehr.
- Belegnummer: {Jahr}/{Nummer} werden URL-kodiert in BELEGNUMMER_*_URL
eingesetzt (Nummer stammt aus Request bzw. gedrucktem Barcode);
belegnummer/release prüft die Typen im Body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
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, string>,
|
||
): 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<void>;
|
||
} = {},
|
||
): Promise<void> {
|
||
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;
|
||
}
|
||
}
|