Zwei Druckbogen desselben Tages hießen beide `2026-09-01_druckbogen.pdf`. Der
Browser hängte „(1)" an; wer sie in denselben Ordner oder aufs NAS legte, verlor
den ersten. Genau der Fehler, der im August für die Bibliotheksbilder behoben
wurde — im Druckmodul stand er noch.
Druckdateien folgen jetzt derselben Regel wie alles andere:
2026-09-01_143207_druck-9-teile_a4_4eca.pdf
Datum, Uhrzeit auf die Sekunde, was auf dem Bogen liegt, das Papier, ein Kürzel.
Das Kürzel ist die Kennung des Verlaufseintrags — eine PDF-Datei lässt sich also
ihrem Eintrag unter „Zuletzt gedruckt" zuordnen. Dafür entsteht die Kennung vor
dem Dateinamen und wird an `recordRun` durchgereicht.
Einzelbilder erben das Motiv ihrer Quelle:
`2026-09-01_143207_lea-badge_130x180_25b3.jpg` statt `2026-09-01_1318.jpg`.
Dafür gibt `loadSourceNamed` neben den Bilddaten auch Namen und Kennung zurück.
Die Oberfläche erfindet keinen eigenen Namen mehr. Der Server schickt einen, der
Client liest ihn aus `Content-Disposition` — es gab zwei Wahrheiten, und die des
Clients war die schlechtere: Sie enthielt nur das Datum. Aus dem Kopf gelesene
Namen werden entschärft, bevor sie in `a.download` landen.
`sheetFilename` in printsource.ts ist entfallen; die Regeln stehen jetzt
vollständig in `naming.ts`, wo sie schon für die Bibliothek stehen.
12 neue Tests, 123 grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
94 lines
4.9 KiB
TypeScript
94 lines
4.9 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { buildSheetFilename, buildSingleFilename, paperToken, nameAusKopf, KLARBILD_NAME } from '../src/lib/naming.ts';
|
|
|
|
/**
|
|
* Namen für Druckdateien. Der Grund für diese Tests ist derselbe wie im August
|
|
* bei den Bibliotheksbildern: Zwei Bogen desselben Tages hießen gleich, und wer
|
|
* beide in denselben Ordner legte, verlor den ersten.
|
|
*/
|
|
|
|
const AM = new Date('2026-09-01T12:32:07Z'); // 14:32:07 Berliner Zeit
|
|
|
|
test('Ein Bogen bekommt Zeitstempel, Inhalt, Papier und Kürzel', () => {
|
|
const n = buildSheetFilename({ wMm: 210, hMm: 297, paperId: 'A4', pieces: 9, pages: 1, runId: '4eca6ec4-…', at: AM });
|
|
assert.equal(n, '2026-09-01_143207_druck-9-teile_a4_4eca.pdf');
|
|
});
|
|
|
|
test('Zwei Bogen derselben Sekunde können sich nicht überschreiben', () => {
|
|
const a = buildSheetFilename({ wMm: 210, hMm: 297, paperId: 'A4', pieces: 9, pages: 1, runId: 'aaaa1111', at: AM });
|
|
const b = buildSheetFilename({ wMm: 210, hMm: 297, paperId: 'A4', pieces: 9, pages: 1, runId: 'bbbb2222', at: AM });
|
|
assert.notEqual(a, b, 'gleiche Sekunde, gleiche Einstellung — trotzdem verschieden');
|
|
});
|
|
|
|
test('Einer und mehrere — Einzahl, Mehrzahl, Bogenzahl', () => {
|
|
const eins = buildSheetFilename({ wMm: 210, hMm: 297, paperId: 'A4', pieces: 1, pages: 1, runId: 'ab12', at: AM });
|
|
assert.ok(eins.includes('druck-1-teil_'), eins);
|
|
const viele = buildSheetFilename({ wMm: 329, hMm: 483, paperId: 'A3PLUS', pieces: 24, pages: 3, runId: 'ffff', at: AM });
|
|
assert.equal(viele, '2026-09-01_143207_druck-24-teile-3-bogen_a3plus_ffff.pdf');
|
|
});
|
|
|
|
test('Eigener Name hat Vorrang und wird entschärft', () => {
|
|
const n = buildSheetFilename({ name: 'Kita Müller / Satz 2', wMm: 210, hMm: 297, paperId: 'A4', pieces: 4, pages: 1, runId: 'cccc', at: AM });
|
|
assert.equal(n, '2026-09-01_143207_kita-mueller-satz-2_a4_cccc.pdf');
|
|
});
|
|
|
|
test('Unbekanntes Papier wird zum Maß', () => {
|
|
assert.equal(paperToken(null, 100, 150), '100x150');
|
|
assert.equal(paperToken('', 210, 297), '210x297');
|
|
assert.equal(paperToken('A4', 210, 297), 'a4');
|
|
// Ein sehr langer Bezeichner würde den Namen aufblähen — dann lieber das Maß.
|
|
assert.equal(paperToken('ein-ellenlanger-bezeichner', 210, 297), '210x297');
|
|
});
|
|
|
|
test('Einzelbilder erben das Motiv der Quelle', () => {
|
|
const n = buildSingleFilename({ sourceName: '2026-08-31_221842_kombiniert_30x40_25b3.png', wMm: 130, hMm: 180, ext: 'jpg', id: '25b34b55-e816', at: AM });
|
|
assert.equal(n, '2026-09-01_143207_kombiniert_130x180_25b3.jpg');
|
|
});
|
|
|
|
test('Ohne brauchbares Motiv steht „klarbild" da, nicht nichts', () => {
|
|
assert.equal(buildSingleFilename({ sourceName: null, wMm: 35, hMm: 45, ext: 'jpg', id: 'aaaa', at: AM }),
|
|
'2026-09-01_143207_klarbild_35x45_aaaa.jpg');
|
|
assert.equal(buildSingleFilename({ sourceName: 'IMG_1234.HEIC', wMm: 35, hMm: 45, ext: 'jpg', id: 'bbbb', at: AM }),
|
|
'2026-09-01_143207_img-1234_35x45_bbbb.jpg');
|
|
});
|
|
|
|
test('Krumme Maße verlieren den Punkt — sonst sähe er wie eine Endung aus', () => {
|
|
const n = buildSingleFilename({ sourceName: 'x.png', wMm: 150, hMm: 112.5, ext: 'png', id: 'dddd', at: AM });
|
|
assert.equal(n, '2026-09-01_143207_x_150x112-5_dddd.png');
|
|
assert.equal(n.split('.').length, 2, 'genau eine Endung');
|
|
});
|
|
|
|
test('Druckdateien folgen demselben Muster wie Bibliotheksergebnisse', () => {
|
|
for (const n of [
|
|
buildSheetFilename({ wMm: 210, hMm: 297, paperId: 'A4', pieces: 9, pages: 1, runId: '4eca', at: AM }),
|
|
buildSingleFilename({ sourceName: 'lea.png', wMm: 130, hMm: 180, ext: 'jpg', id: '25b3', at: AM }),
|
|
]) {
|
|
assert.ok(KLARBILD_NAME.test(n.replace(/\.[^.]+$/, '')), `passt nicht ins Schema: ${n}`);
|
|
}
|
|
});
|
|
|
|
test('Namen taugen für jedes Dateisystem', () => {
|
|
for (const name of ['Grüße/Köln', 'a\\b:c*d?e"f<g>h|i', '../../..', 'x'.repeat(200)]) {
|
|
const n = buildSheetFilename({ name, wMm: 210, hMm: 297, paperId: 'A4', pieces: 2, pages: 1, runId: 'eeee', at: AM });
|
|
assert.ok(/^[a-z0-9_.-]+$/.test(n), `unerwartetes Zeichen: ${n}`);
|
|
assert.ok(n.endsWith('.pdf'));
|
|
}
|
|
});
|
|
|
|
test('Der Name kommt vom Server, nicht aus dem Browser', () => {
|
|
assert.equal(nameAusKopf('attachment; filename="2026-09-01_143207_druck-9-teile_a4_4eca.pdf"', 'x.pdf'),
|
|
'2026-09-01_143207_druck-9-teile_a4_4eca.pdf');
|
|
assert.equal(nameAusKopf("attachment; filename*=UTF-8''gr%C3%BC%C3%9Fe.pdf", 'x.pdf'), 'grüße.pdf');
|
|
assert.equal(nameAusKopf(null, 'x.pdf'), 'x.pdf');
|
|
assert.equal(nameAusKopf('attachment', 'x.pdf'), 'x.pdf');
|
|
});
|
|
|
|
test('Ein Name aus dem Kopf kann keinen Pfad einschleusen', () => {
|
|
for (const boese of ['attachment; filename="../../etc/passwd"', 'attachment; filename="a/b.pdf"',
|
|
'attachment; filename="a\\\\b.pdf"', 'attachment; filename=".hidden"']) {
|
|
const n = nameAusKopf(boese, 'x.pdf');
|
|
assert.ok(!n.includes('/') && !n.includes('\\') && !n.startsWith('.'), `Pfad überlebt: ${n}`);
|
|
}
|
|
});
|