From 545f9ae909edd5285cd2bee9901bd204282bf8b7 Mon Sep 17 00:00:00 2001 From: Till Heidrich Date: Tue, 1 Sep 2026 15:39:39 +0000 Subject: [PATCH] =?UTF-8?q?Bibliothek:=20eindeutige=20Namen=20auch=20f?= =?UTF-8?q?=C3=BCr=20Auswahl-ZIPs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dieselbe Schwäche eine Ebene weiter: Jedes ZIP aus der Bibliothek hieß `klarbild_2026-09-01.zip`, zwei verschiedene Auswahlen am selben Tag im selben Ordner überschrieben sich also. Jetzt `2026-09-01_143207_auswahl-12-bilder_a3f9.zip`, und der Client liest den Namen aus dem Kopf statt einen eigenen zu bauen. 124 Tests grün. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1 --- src/components/LibraryApp.tsx | 5 ++++- src/lib/naming.ts | 12 ++++++++++++ src/pages/api/items/zip.ts | 6 ++++-- tests/printnames.test.ts | 10 ++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/LibraryApp.tsx b/src/components/LibraryApp.tsx index dba8e29..41eb799 100644 --- a/src/components/LibraryApp.tsx +++ b/src/components/LibraryApp.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; +import { nameAusKopf } from '../lib/naming'; import ShareDialog, { type ShareZiel } from './ShareDialog'; interface Item { id: string; filename: string; output_px: string; has_alpha: boolean; @@ -77,7 +78,9 @@ export default function LibraryApp() { if (!res.ok) { notify('ZIP fehlgeschlagen.'); return; } const blob = await res.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); - a.download = `klarbild_${new Date().toISOString().slice(0, 10)}.zip`; a.click(); + // Der Name kommt vom Server — hier einen eigenen zu bauen hieße, zwei + // Auswahlen desselben Tages wieder gleich zu nennen. + a.download = nameAusKopf(res.headers.get('Content-Disposition'), 'klarbild-auswahl.zip'); a.click(); URL.revokeObjectURL(a.href); notify('ZIP geladen.'); }; const alternative = async (id: string) => { diff --git a/src/lib/naming.ts b/src/lib/naming.ts index 35d2012..22c1b41 100644 --- a/src/lib/naming.ts +++ b/src/lib/naming.ts @@ -230,3 +230,15 @@ export function nameAusKopf(header: string | null | undefined, fallback: string) function sicher(n: string): string { return n.replace(/[\\/\r\n\0]/g, '').replace(/^\.+/, '').trim().slice(0, 120); } + +/** + * Name für ein ZIP aus einer Auswahl in der Bibliothek: + * `2026-09-01_143207_auswahl-12-bilder_a3f9.zip` + * + * Vorher hieß jedes `klarbild_2026-09-01.zip` — zwei verschiedene Auswahlen am + * selben Tag im selben Ordner, und die zweite überschrieb die erste. + */ +export function buildAuswahlZipName(anzahl: number, at: Date = new Date()): string { + const was = anzahl === 1 ? 'auswahl-1-bild' : `auswahl-${anzahl}-bilder`; + return `${timeStamp(at)}_${was}_${shortTag(null)}.zip`; +} diff --git a/src/pages/api/items/zip.ts b/src/pages/api/items/zip.ts index 617b2b6..8bfd4dd 100644 --- a/src/pages/api/items/zip.ts +++ b/src/pages/api/items/zip.ts @@ -1,4 +1,5 @@ import type { APIRoute } from 'astro'; +import { buildAuswahlZipName } from '../../../lib/naming'; import archiver from 'archiver'; import { query } from '../../../lib/db'; import { getObject } from '../../../lib/storage'; @@ -40,11 +41,12 @@ export const POST: APIRoute = async ({ request, locals }) => { done.catch(() => {}); }, }); - const stamp = new Date().toISOString().slice(0, 10); + // Eindeutig statt nur datiert: Zwei Auswahlen desselben Tages hießen gleich. + const name = buildAuswahlZipName(rows.length); return new Response(stream, { headers: { 'Content-Type': 'application/zip', - 'Content-Disposition': `attachment; filename="klarbild_${stamp}.zip"`, + 'Content-Disposition': `attachment; filename="${name}"`, }, }); }; diff --git a/tests/printnames.test.ts b/tests/printnames.test.ts index a25f720..d30c68b 100644 --- a/tests/printnames.test.ts +++ b/tests/printnames.test.ts @@ -91,3 +91,13 @@ test('Ein Name aus dem Kopf kann keinen Pfad einschleusen', () => { assert.ok(!n.includes('/') && !n.includes('\\') && !n.startsWith('.'), `Pfad überlebt: ${n}`); } }); + +test('ZIPs aus der Bibliothek heißen nicht alle gleich', async () => { + const { buildAuswahlZipName } = await import('../src/lib/naming.ts'); + const a = buildAuswahlZipName(12, AM); + assert.match(a, /^2026-09-01_143207_auswahl-12-bilder_[0-9a-f]{4}\.zip$/); + assert.equal(buildAuswahlZipName(1, AM).includes('auswahl-1-bild_'), true); + // Zwei Auswahlen derselben Sekunde: das Kürzel unterscheidet sie. + const paare = new Set(Array.from({ length: 50 }, () => buildAuswahlZipName(12, AM))); + assert.ok(paare.size > 40, `zu viele gleiche Namen: ${paare.size}/50`); +});