3 Commits
Author SHA1 Message Date
Florian WildeandClaude Opus 5.5 5fbf1ac4de Fassung 1.0.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:50:59 +02:00
Florian WildeandClaude Opus 5.5 adb93a722f Kontowerkzeug ins Abbild, Containerabnahme
Die README nannte `node werkzeuge/konto.js`, im Abbild lag aber nur das
Sicherungswerkzeug, und tsx gibt es dort nicht. Ein frisch aufgesetzter
Server stand ohne jedes Konto da. Das Kontoskript wird jetzt beim Bau mit
esbuild zu einer Datei gebuendelt; die nativen Module bleiben aussen vor,
sie bringt die Standalone-Ausgabe mit.

Aufgefallen ist das nicht, weil der Abnahmelauf gegen `next start` prueft.
Neu ist deshalb `npm run abnahme:container`: Abbild bauen, mit leerem
Volume starten, erstes Konto anlegen, anmelden, Passwort neu setzen,
sichern.

Ausserdem trennt saubererName Windows-Pfade jetzt auch unter Linux ab
(path.win32.basename). Der Test dazu war nur unter Windows gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:50:59 +02:00
Florian WildeandClaude Opus 5 6181ec4bd7 Pruefskripte: wiederholbar und ehrlich machen
Die Dichtheitspruefung der Musterloesungen las die Server-Nutzlast aus
self.__next_f - das Feld leert Next nach der Verarbeitung, es war also
immer leer. Die Pruefung lief an dieser Stelle ins Leere, ohne dass es
auffiel; nur der HTML-Quelltext wurde wirklich geprueft. Gelesen wird
jetzt der Quelltext der eingebetteten Skripte. Damit zeigt die Ausgabe,
was sie zeigen soll: vor der Abgabe steht fuenfmal "musterloesung":null
in der Nutzlast, danach stehen dort die Loesungen.

Ausserdem stellt das Skript seine Ausgangslage selbst her und laeuft
gegen den Port, der ueber LERNBAR_BASIS eingestellt ist - vorher war
3311 fest eingetragen und ein zweiter Lauf scheiterte daran, dass die
Kontrolle schon abgegeben war.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 19:36:29 +02:00
9 changed files with 265 additions and 20 deletions
+1
View File
@@ -1,6 +1,7 @@
node_modules/ node_modules/
.next/ .next/
out/ out/
build/
next-env.d.ts next-env.d.ts
*.tsbuildinfo *.tsbuildinfo
+23
View File
@@ -26,6 +26,29 @@ Vor `npm version` wandert der Abschnitt „Unveröffentlicht" unter eine neue
## [Unveröffentlicht] ## [Unveröffentlicht]
## [1.0.1] – 2026-10-03
Das Docker-Abbild ist jetzt aus dem Nichts aufsetzbar.
### Behoben
- **Im Docker-Abbild ließ sich kein erstes Konto anlegen.** Die README nannte
`node werkzeuge/konto.js`, das Abbild enthielt aber nur die Sicherungsskripte,
und `tsx` gibt es dort nicht. Ein frisch aufgesetzter Server stand vor einer
Anmeldeseite ohne Konto. Das Kontowerkzeug wird jetzt beim Bau mit esbuild zu
einer Datei gebündelt (`npm run werkzeuge:bauen`) und liegt unter
`/app/werkzeuge/konto.js`
- **Windows-Pfade in Dateinamen von Anhängen** wurden unter Linux nicht
abgetrennt – aus `C:\Users\x\foto.jpg` wurde `C:Usersxfoto.jpg`. Nur die
Anzeige war betroffen, gespeichert wird unter der UUID. Der Test dazu war
unter Windows grün und unter Linux rot
### Hinzugefügt
- `npm run abnahme:container` – Abnahmelauf gegen das gebaute Docker-Abbild:
Lebenszeichen, Betrieb ohne root, erstes Konto, Anmeldung, Passwort neu
setzen, Sicherung im Volume
## [1.0.0] – 2026-09-11 ## [1.0.0] – 2026-09-11
Die erste abnahmefähige Fassung. Alle zehn Bausteine sind fertig, die Die erste abnahmefähige Fassung. Alle zehn Bausteine sind fertig, die
+5 -1
View File
@@ -24,6 +24,9 @@ ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=abhaengigkeiten /app/node_modules ./node_modules COPY --from=abhaengigkeiten /app/node_modules ./node_modules
COPY . . COPY . .
RUN npm run build RUN npm run build
# Das Kontowerkzeug als eine einzige JS-Datei – im Abbild gibt es kein tsx.
# Die nativen Module bleiben draußen, sie bringt die Standalone-Ausgabe mit.
RUN npm run werkzeuge:bauen
# --- Laufzeit ------------------------------------------------------------- # --- Laufzeit -------------------------------------------------------------
FROM node:${NODE_VERSION} AS laufzeit FROM node:${NODE_VERSION} AS laufzeit
@@ -51,8 +54,9 @@ COPY --from=bau /app/public ./public
COPY --from=bau /app/content ./content COPY --from=bau /app/content ./content
COPY --from=bau /app/src/lib/db/migrationen ./migrationen COPY --from=bau /app/src/lib/db/migrationen ./migrationen
# Werkzeuge für Sicherung und Wiederherstellung im Container. # Werkzeuge für Sicherung, Wiederherstellung und das erste Konto.
COPY --from=bau /app/scripts/im-container ./werkzeuge COPY --from=bau /app/scripts/im-container ./werkzeuge
COPY --from=bau /app/build/werkzeuge/konto.js ./werkzeuge/konto.js
RUN chmod +x ./werkzeuge/*.sh RUN chmod +x ./werkzeuge/*.sh
RUN mkdir -p /daten && chown -R node:node /daten /app RUN mkdir -p /daten && chown -R node:node /daten /app
+16 -6
View File
@@ -39,14 +39,13 @@ Die Anwendung lauscht danach auf `127.0.0.1:3000`. Erstes Ausbilderkonto
anlegen: anlegen:
```bash ```bash
docker compose exec lernbar node -e "1" >/dev/null # Container erreichbar? docker compose exec lernbar node werkzeuge/konto.js \
docker compose exec lernbar sh -c \ --name "Vorname Nachname" --email ausbilder@betrieb.de --rolle ausbilder
'node werkzeuge/konto.js --name "Vorname Nachname" --email ausbilder@betrieb.de --rolle ausbilder'
``` ```
> Solange das Konto-Werkzeug im Abbild noch nicht liegt, geht es vom Ohne `--passwort` wird eines erzeugt und ausgegeben. Gibt es die Adresse
> Entwicklungsrechner aus: schon, setzt derselbe Aufruf das Passwort neu – das ist zugleich der Weg, wenn
> `npm run konto:anlegen -- --name "…" --email … --rolle ausbilder` das einzige Ausbilderkonto sein Passwort vergessen hat.
Danach im Browser anmelden, unter **Verwaltung** das Azubikonto anlegen und Danach im Browser anmelden, unter **Verwaltung** das Azubikonto anlegen und
das Ausbildungsverhältnis mit Beginndatum einrichten. das Ausbildungsverhältnis mit Beginndatum einrichten.
@@ -244,6 +243,7 @@ npm run typecheck # TypeScript
npm run lint # ESLint npm run lint # ESLint
npm run test # Vitest – Logik, die wehtut (315 Tests) npm run test # Vitest – Logik, die wehtut (315 Tests)
npm run abnahme # der Abnahmelauf, siehe unten npm run abnahme # der Abnahmelauf, siehe unten
npm run abnahme:container # derselbe Weg gegen das Docker-Abbild
``` ```
### Der Abnahmelauf ### Der Abnahmelauf
@@ -269,6 +269,16 @@ Voraussetzung: `npm run build` ist gelaufen, und Port 3399 ist frei – sonst
bricht der Lauf mit einer Meldung ab, statt gegen einen fremden Datenbestand zu bricht der Lauf mit einer Meldung ab, statt gegen einen fremden Datenbestand zu
prüfen. prüfen.
### Der Containerlauf
`npm run abnahme` prüft über `next start`, betrieben wird aber der Container.
`npm run abnahme:container` baut deshalb das Abbild, startet es mit leerem
Volume und geht den Weg aus dieser README: Lebenszeichen, erstes Konto über
`werkzeuge/konto.js`, Anmeldung, Passwort neu setzen, Sicherung im Container.
Danach werden Container, Volume und Abbild wieder entfernt.
Voraussetzung: Docker läuft, Port 3398 ist frei (`LERNBAR_CONTAINER_PORT`).
### Einzelne Zusagen ### Einzelne Zusagen
```bash ```bash
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "lernbar", "name": "lernbar",
"version": "0.1.0", "version": "1.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lernbar", "name": "lernbar",
"version": "0.1.0", "version": "1.0.1",
"dependencies": { "dependencies": {
"@node-rs/argon2": "^2.0.2", "@node-rs/argon2": "^2.0.2",
"better-sqlite3": "^12.11.1", "better-sqlite3": "^12.11.1",
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "lernbar", "name": "lernbar",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"description": "Lern-Bar – betriebliche Ausbildung Fachinformatiker Systemintegration", "description": "Lern-Bar – betriebliche Ausbildung Fachinformatiker Systemintegration",
"engines": { "engines": {
@@ -17,11 +17,13 @@
"e2e": "playwright test", "e2e": "playwright test",
"inhalte:pruefen": "tsx scripts/inhalte-pruefen.ts", "inhalte:pruefen": "tsx scripts/inhalte-pruefen.ts",
"konto:anlegen": "tsx scripts/konto-anlegen.ts", "konto:anlegen": "tsx scripts/konto-anlegen.ts",
"werkzeuge:bauen": "esbuild scripts/konto-anlegen.ts --bundle --platform=node --target=node22 --format=cjs --external:better-sqlite3 --external:@node-rs/argon2 --outfile=build/werkzeuge/konto.js",
"backup": "bash scripts/im-container/sicherung.sh", "backup": "bash scripts/im-container/sicherung.sh",
"restore": "bash scripts/im-container/wiederherstellen.sh", "restore": "bash scripts/im-container/wiederherstellen.sh",
"screenshots": "tsx scripts/screenshots.ts", "screenshots": "tsx scripts/screenshots.ts",
"symbole": "tsx scripts/symbole.ts", "symbole": "tsx scripts/symbole.ts",
"abnahme": "node scripts/pruefungen/abnahme.mjs" "abnahme": "node scripts/pruefungen/abnahme.mjs",
"abnahme:container": "node scripts/pruefungen/container.mjs"
}, },
"dependencies": { "dependencies": {
"@node-rs/argon2": "^2.0.2", "@node-rs/argon2": "^2.0.2",
+165
View File
@@ -0,0 +1,165 @@
import { chromium } from '@playwright/test';
import { spawnSync } from 'node:child_process';
/**
* Der Abnahmelauf gegen das Docker-Abbild.
*
* `abnahme.mjs` prüft die Anwendung über `next start`. Betrieben wird sie aber
* als Container – und was dort fehlt, fällt dabei nicht auf. So war es mit dem
* Kontowerkzeug: Die README beschrieb `node werkzeuge/konto.js`, im Abbild lag
* es nie, und ein frisch aufgesetzter Server stand ohne jedes Konto da.
*
* Dieser Lauf baut das Abbild, startet es mit leerem Volume und geht den Weg
* aus der README: Lebenszeichen, erstes Konto, Anmeldung, Sicherung.
*
* node scripts/pruefungen/container.mjs
*
* Voraussetzung: Docker läuft, Port 3398 ist frei (LERNBAR_CONTAINER_PORT).
*/
const PORT = Number(process.env.LERNBAR_CONTAINER_PORT ?? 3398);
const BASIS = `http://localhost:${PORT}`;
const ABBILD = 'lernbar:abnahme';
const NAME = `lernbar-abnahme-${Date.now()}`;
const AUSBILDER = { name: 'Petra Kessler', email: 'petra.kessler@betrieb.test', pw: 'container-ausbilder' };
let fehler = 0;
function pruefe(bedingung, text, zusatz = '') {
console.log(` ${bedingung ? '✓' : '✗'} ${text}${zusatz ? ` – ${zusatz}` : ''}`);
if (!bedingung) fehler += 1;
}
function abschnitt(titel) {
console.log(`\n${titel}`);
}
function docker(argumente, zusatz = {}) {
return spawnSync('docker', argumente, { encoding: 'utf8', ...zusatz });
}
function imContainer(...argumente) {
const ergebnis = docker(['exec', NAME, ...argumente]);
return { ende: ergebnis.status, ausgabe: `${ergebnis.stdout}${ergebnis.stderr}` };
}
let browser = null;
function aufraeumen() {
// -v nimmt das namenlose Volume mit – sonst bleibt bei jedem Lauf eins liegen.
docker(['rm', '-fv', NAME], { stdio: 'ignore' });
docker(['rmi', ABBILD], { stdio: 'ignore' });
}
async function portBelegt() {
try {
await fetch(`${BASIS}/api/gesundheit`, { signal: AbortSignal.timeout(1000) });
return true;
} catch {
return false;
}
}
async function gesund(sekunden = 60) {
for (let i = 0; i < sekunden; i += 1) {
try {
const antwort = await fetch(`${BASIS}/api/gesundheit`);
if (antwort.ok) return true;
} catch {
/* noch nicht da */
}
await new Promise((r) => setTimeout(r, 1000));
}
return false;
}
async function anmelden(email, passwort) {
const kontext = await browser.newContext({ locale: 'de-DE' });
const seite = await kontext.newPage();
seite.setDefaultTimeout(30000);
await seite.goto(`${BASIS}/anmelden`);
await seite.fill('#email', email);
await seite.fill('#passwort', passwort);
await seite.click('button[type=submit]');
const angekommen = await seite
.waitForURL(`${BASIS}/`, { timeout: 15000 })
.then(() => true)
.catch(() => false);
await kontext.close();
return angekommen;
}
async function main() {
if (docker(['info'], { stdio: 'ignore' }).status !== 0) {
console.error('Docker ist nicht erreichbar.');
process.exit(2);
}
if (await portBelegt()) {
console.error(`Auf Port ${PORT} antwortet schon etwas. Anderen Port über LERNBAR_CONTAINER_PORT setzen.`);
process.exit(2);
}
// --- C1 Das Abbild entsteht ----------------------------------------------
abschnitt('C1 · Das Abbild entsteht und startet');
console.log(' … baue das Abbild, das dauert beim ersten Mal einige Minuten');
const bau = docker(['build', '-t', ABBILD, '.'], { stdio: ['ignore', 'ignore', 'pipe'] });
pruefe(bau.status === 0, 'Das Abbild lässt sich bauen', bau.status === 0 ? '' : bau.stderr.split('\n').slice(-5).join(' '));
if (bau.status !== 0) return;
const start = docker(['run', '-d', '--name', NAME, '-p', `127.0.0.1:${PORT}:3000`, ABBILD]);
pruefe(start.status === 0, 'Der Container startet');
if (start.status !== 0) return;
pruefe(await gesund(), 'Die Anwendung meldet sich gesund');
pruefe(imContainer('id', '-un').ausgabe.trim() === 'node', 'Sie läuft nicht als root');
// --- C2 Das erste Konto ---------------------------------------------------
abschnitt('C2 · Das erste Konto entsteht im Container');
const werkzeuge = imContainer('ls', '/app/werkzeuge').ausgabe;
pruefe(werkzeuge.includes('konto.js'), 'Das Kontowerkzeug liegt im Abbild');
const anlegen = imContainer(
'node', 'werkzeuge/konto.js',
'--name', AUSBILDER.name, '--email', AUSBILDER.email, '--rolle', 'ausbilder', '--passwort', AUSBILDER.pw,
);
pruefe(anlegen.ende === 0 && anlegen.ausgabe.includes('Konto angelegt'), 'Der Aufruf aus der README legt das Konto an', anlegen.ausgabe.split('\n')[0]);
browser = await chromium.launch();
pruefe(await anmelden(AUSBILDER.email, AUSBILDER.pw), 'Der Ausbilder meldet sich mit diesem Konto an');
pruefe(!(await anmelden(AUSBILDER.email, 'falsch-falsch')), 'Ein falsches Passwort wird abgewiesen');
// Derselbe Aufruf ist der Rettungsweg, wenn das einzige Ausbilderkonto sein
// Passwort vergessen hat.
const neu = imContainer('node', 'werkzeuge/konto.js', '--name', AUSBILDER.name, '--email', AUSBILDER.email);
const neuesPw = neu.ausgabe.match(/Passwort: (\S+)/)?.[1];
pruefe(neu.ende === 0 && neu.ausgabe.includes('gab es schon') && neuesPw, 'Ein zweiter Aufruf setzt das Passwort neu');
pruefe(neuesPw && (await anmelden(AUSBILDER.email, neuesPw)), 'Mit dem neuen Passwort geht die Anmeldung');
pruefe(!(await anmelden(AUSBILDER.email, AUSBILDER.pw)), 'Das alte Passwort gilt nicht mehr');
// --- C3 Sicherung im Container -------------------------------------------
abschnitt('C3 · Sicherung im Container');
const sicherung = imContainer('/app/werkzeuge/sicherung.sh', '/daten/sicherungen');
pruefe(sicherung.ende === 0, 'Die Sicherung läuft im Container durch', sicherung.ausgabe.trim().split('\n').at(-1));
const archive = imContainer('sh', '-c', 'ls /daten/sicherungen/lernbar-*.tar.gz').ausgabe;
pruefe(/lernbar-\d{8}-\d{6}\.tar\.gz/.test(archive), 'Das Archiv liegt im Volume');
}
try {
await main();
} catch (e) {
console.error(e);
fehler += 1;
} finally {
if (browser) await browser.close();
aufraeumen();
}
console.log(fehler === 0 ? '\n✓ Containerabnahme bestanden.' : `\n✗ ${fehler} Prüfung(en) nicht erfüllt.`);
process.exit(fehler === 0 ? 0 : 1);
+45 -7
View File
@@ -11,7 +11,7 @@ import path from 'node:path';
* im sichtbaren Text. * im sichtbaren Text.
*/ */
const BASIS = 'http://localhost:3311'; const BASIS = process.env.LERNBAR_BASIS ?? 'http://localhost:3000';
const AZUBI = { email: 'jonas.berger@betrieb.de', pw: 'azubi-start-2026' }; const AZUBI = { email: 'jonas.berger@betrieb.de', pw: 'azubi-start-2026' };
const AUSBILDER = { email: 'fw@wildeflorian.de', pw: 'ausbilder-start' }; const AUSBILDER = { email: 'fw@wildeflorian.de', pw: 'ausbilder-start' };
@@ -26,6 +26,29 @@ const bruchstuecke = loesungen.map((l) => l.slice(40, 110)).filter((b) => b.leng
console.log(`${loesungen.length} Musterlösungen aus woche-01.yaml gelesen.`); console.log(`${loesungen.length} Musterlösungen aus woche-01.yaml gelesen.`);
console.log('Geprüft wird auf Bruchstücke wie:', JSON.stringify(bruchstuecke[0]?.slice(0, 60))); console.log('Geprüft wird auf Bruchstücke wie:', JSON.stringify(bruchstuecke[0]?.slice(0, 60)));
// --- Ausgangslage herstellen ----------------------------------------------
//
// Der entscheidende Fall ist „freigegeben, aber noch nicht abgegeben". Den gibt
// es nur einmal je Kontrolle. Damit die Prüfung wiederholbar bleibt, wird die
// Kontrolle „woche-01" vorher zurückgesetzt – das ist ein Testaufbau, kein Teil
// der Anwendung, und greift deshalb ausdrücklich nur auf die lokale
// Beispieldatenbank zu.
const { default: Datenbank } = await import('better-sqlite3');
const dbPfad = process.env.LERNBAR_DB ?? path.join(process.cwd(), 'daten', 'lernbar.sqlite');
if (fs.existsSync(dbPfad)) {
const db = new Datenbank(dbPfad);
const betroffen = db.prepare("select id from kontrolle where kontrolle_id = 'woche-01'").all();
for (const eintrag of betroffen) {
db.prepare('delete from kontroll_antwort where kontrolle_id = ?').run(eintrag.id);
db.prepare('delete from kontrolle where id = ?').run(eintrag.id);
}
db.close();
console.log(`Ausgangslage hergestellt: ${betroffen.length} frühere Bearbeitung(en) von woche-01 entfernt.`);
} else {
console.log(`Keine Datenbank unter ${dbPfad} – die Ausgangslage wird nicht zurückgesetzt.`);
}
const browser = await chromium.launch(); const browser = await chromium.launch();
const seite = await (await browser.newContext({ locale: 'de-DE' })).newPage(); const seite = await (await browser.newContext({ locale: 'de-DE' })).newPage();
@@ -41,13 +64,19 @@ async function abmelden() {
await seite.waitForURL(/anmelden/); await seite.waitForURL(/anmelden/);
} }
/** Sucht im rohen HTML **und** in der RSC-Nutzlast. */ /** Sucht im rohen HTML **und** in der Server-Nutzlast. */
async function lecksuche(beschriftung) { async function lecksuche(beschriftung) {
const html = await seite.content(); const html = await seite.content();
// Die Nutzlast steht in eingebetteten Skripten, die `self.__next_f` füllen.
// Dort **nachträglich** nachzusehen bringt nichts: Next leert das Feld, sobald
// es verarbeitet ist – es wäre immer leer, und die Prüfung liefe ins Leere,
// ohne dass es auffällt. Deshalb wird der Quelltext der Skripte gelesen.
const rsc = await seite.evaluate(() => const rsc = await seite.evaluate(() =>
// Next legt die Server-Nutzlast in self.__next_f ab – dort steht alles, [...document.querySelectorAll('script:not([src])')]
// was der Server an den Browser geschickt hat. .map((element) => element.textContent ?? '')
(globalThis.self?.__next_f ?? []).map((t) => (Array.isArray(t) ? String(t[1] ?? '') : '')).join(''), .filter((text) => text.includes('__next_f'))
.join(''),
); );
const treffer = bruchstuecke.filter((b) => html.includes(b) || rsc.includes(b)); const treffer = bruchstuecke.filter((b) => html.includes(b) || rsc.includes(b));
@@ -55,7 +84,7 @@ async function lecksuche(beschriftung) {
console.log( console.log(
` ${beschriftung.padEnd(34)} Leck: ${treffer.length === 0 ? 'nein' : `JA (${treffer.length})`}` + ` ${beschriftung.padEnd(34)} Leck: ${treffer.length === 0 ? 'nein' : `JA (${treffer.length})`}` +
` | RSC-Größe ${rsc.length} | "musterloesung":null ${nullFelder}x`, ` | Nutzlast ${rsc.length} Zeichen | "musterloesung":null ${nullFelder}x`,
); );
return treffer; return treffer;
} }
@@ -71,8 +100,17 @@ await abmelden();
await anmelden(AUSBILDER); await anmelden(AUSBILDER);
await seite.goto(`${BASIS}/kontrollen`); await seite.goto(`${BASIS}/kontrollen`);
await seite.waitForSelector('.kontrollzeile'); await seite.waitForSelector('.kontrollzeile');
await seite.locator('.kontrollzeile', { hasText: 'Woche 1' }).locator('button:has-text("Freigeben")').click(); const freigeben = seite
.locator('.kontrollzeile', { hasText: 'Woche 1' })
.locator('button:has-text("Freigeben")');
if ((await freigeben.count()) > 0) {
await freigeben.click();
await seite.waitForTimeout(1500); await seite.waitForTimeout(1500);
} else {
// Bei einem wiederholten Lauf ist sie schon freigegeben. Das ist kein
// Grund abzubrechen – der entscheidende Fall kommt erst danach.
console.log(' (war bereits freigegeben)');
}
console.log('\n3) Nach der Freigabe, vor der Abgabe – der entscheidende Fall'); console.log('\n3) Nach der Freigabe, vor der Abgabe – der entscheidende Fall');
await abmelden(); await abmelden();
+3 -1
View File
@@ -67,7 +67,9 @@ export function megabyte(bytes: number): string {
* etwas verloren. * etwas verloren.
*/ */
export function saubererName(name: string): string { export function saubererName(name: string): string {
return path // win32.basename trennt an / und \ – der Name kommt vom Browser, nicht vom
// Server, und ein Windows-Pfad darf auch unter Linux nicht durchrutschen.
return path.win32
.basename(name) .basename(name)
.replace(/[\u0000-\u001f\u007f]/g, '') .replace(/[\u0000-\u001f\u007f]/g, '')
.replace(/["\\]/g, '') .replace(/["\\]/g, '')