feat: bulk ZIP download, bulk move-to-folder, folder rename
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
@@ -21,6 +21,20 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
return json({ folder: row });
|
||||
};
|
||||
|
||||
// Ordner umbenennen / Galerie ändern. Body { id, name?, picdrop_gallery? }
|
||||
export const PATCH: APIRoute = async ({ request, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const b = await request.json();
|
||||
if (!b.id) return json({ error: 'Keine ID.' }, 400);
|
||||
const sets: string[] = []; const args: any[] = [];
|
||||
if (typeof b.name === 'string' && b.name.trim()) { args.push(b.name.trim()); sets.push(`name=$${args.length}`); }
|
||||
if ('picdrop_gallery' in b) { args.push(b.picdrop_gallery?.trim() || null); sets.push(`picdrop_gallery=$${args.length}`); }
|
||||
if (!sets.length) return json({ error: 'Nichts zu ändern.' }, 400);
|
||||
args.push(b.id);
|
||||
const row = await one(`UPDATE folders SET ${sets.join(',')} WHERE id=$${args.length} RETURNING *`, args);
|
||||
return json({ folder: row });
|
||||
};
|
||||
|
||||
// Ordner löschen (Bilder bleiben, verlieren nur die Zuordnung). ?id=…
|
||||
export const DELETE: APIRoute = async ({ request, url, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import archiver from 'archiver';
|
||||
import { query } from '../../../lib/db';
|
||||
import { getObject } from '../../../lib/storage';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
/** Lädt mehrere Ergebnisse als ZIP. Body { itemIds: [...] } */
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const b = await request.json().catch(() => ({} as any));
|
||||
const ids: string[] = b.itemIds || [];
|
||||
if (!ids.length) return new Response('Keine Auswahl.', { status: 400 });
|
||||
|
||||
const rows = await query<{ id: string; result_path: string | null; filename: string | null }>(
|
||||
`SELECT id, result_path, filename FROM items WHERE id = ANY($1) AND result_path IS NOT NULL`, [ids]);
|
||||
if (!rows.length) return new Response('Keine Ergebnisse.', { status: 404 });
|
||||
|
||||
const archive = archiver('zip', { zlib: { level: 6 } });
|
||||
const seen: Record<string, number> = {};
|
||||
for (const r of rows) {
|
||||
try {
|
||||
const buf = await getObject(r.result_path!);
|
||||
let name = r.filename || `${r.id}.png`;
|
||||
if (!/\.[a-z0-9]+$/i.test(name)) name += '.png';
|
||||
// Namenskollisionen vermeiden
|
||||
if (seen[name] != null) { const n = ++seen[name]; name = name.replace(/(\.[^.]+)$/, `_${n}$1`); }
|
||||
else seen[name] = 0;
|
||||
archive.append(buf, { name });
|
||||
} catch { /* Datei überspringen */ }
|
||||
}
|
||||
const done = archive.finalize();
|
||||
|
||||
// Archiver-Stream (Node) in einen Web-ReadableStream überführen.
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
archive.on('data', (c: Buffer) => controller.enqueue(new Uint8Array(c)));
|
||||
archive.on('end', () => controller.close());
|
||||
archive.on('error', (e: any) => controller.error(e));
|
||||
done.catch(() => {});
|
||||
},
|
||||
});
|
||||
const stamp = new Date().toISOString().slice(0, 10);
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'application/zip',
|
||||
'Content-Disposition': `attachment; filename="klarbild_${stamp}.zip"`,
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user