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:
4 files changed
+30
-3
No files matched your search
@@ -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) => {
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
}
|
||||||
@@ -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}"`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -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`);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user