Bibliothek: eindeutige Namen auch für Auswahl-ZIPs

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
This commit is contained in:
tillandClaude Opus 5 committed 2026-09-01 15:39:39 +00:00
1 parent a87587c273
commit 545f9ae909
4 files changed
+30 -3

No files matched your search

+4 -1
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { nameAusKopf } from '../lib/naming';
import ShareDialog, { type ShareZiel } from './ShareDialog'; import ShareDialog, { type ShareZiel } from './ShareDialog';
interface Item { id: string; filename: string; output_px: string; has_alpha: boolean; 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; } if (!res.ok) { notify('ZIP fehlgeschlagen.'); return; }
const blob = await res.blob(); const blob = await res.blob();
const a = document.createElement('a'); a.href = URL.createObjectURL(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.'); URL.revokeObjectURL(a.href); notify('ZIP geladen.');
}; };
const alternative = async (id: string) => { const alternative = async (id: string) => {
+12
View File
@@ -230,3 +230,15 @@ export function nameAusKopf(header: string | null | undefined, fallback: string)
function sicher(n: string): string { function sicher(n: string): string {
return n.replace(/[\\/\r\n\0]/g, '').replace(/^\.+/, '').trim().slice(0, 120); 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`;
}
+4 -2
View File
@@ -1,4 +1,5 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { buildAuswahlZipName } from '../../../lib/naming';
import archiver from 'archiver'; import archiver from 'archiver';
import { query } from '../../../lib/db'; import { query } from '../../../lib/db';
import { getObject } from '../../../lib/storage'; import { getObject } from '../../../lib/storage';
@@ -40,11 +41,12 @@ export const POST: APIRoute = async ({ request, locals }) => {
done.catch(() => {}); 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, { return new Response(stream, {
headers: { headers: {
'Content-Type': 'application/zip', 'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="klarbild_${stamp}.zip"`, 'Content-Disposition': `attachment; filename="${name}"`,
}, },
}); });
}; };
+10
View File
@@ -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}`); 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`);
});