import type { APIRoute } from 'astro'; import sharp from 'sharp'; import { one } from '../../../../lib/db'; import { getObject } from '../../../../lib/storage'; import { resolveShare, shareHasItem, trackShare } from '../../../../lib/shares'; export const prerender = false; /** * Bild hinter einem Teilen-Link ausliefern — **ohne Anmeldung**. * * Die Zugriffsprüfung hängt allein am Link, nicht am Bild: Nur was in * `share_items` steht, kommt heraus. Damit kann niemand über einen gültigen Link * fremde Bilder abrufen, indem er eine andere Bild-Kennung einsetzt. */ export const GET: APIRoute = async ({ params, url, request, clientAddress }) => { const q = url.searchParams; const { status, share } = await resolveShare(params.slug!, q.get('pw')); if (status !== 'ok' || !share) { return new Response( status === 'abgelaufen' ? 'Dieser Link ist abgelaufen.' : status === 'widerrufen' ? 'Dieser Link wurde deaktiviert.' : status === 'passwort' ? 'Zugangswort nötig.' : 'Nicht gefunden.', { status: status === 'passwort' ? 401 : status === 'nicht-gefunden' ? 404 : 410 }); } const itemId = q.get('item'); if (!itemId || !/^[0-9a-f-]{36}$/i.test(itemId)) return new Response('Nicht gefunden.', { status: 404 }); if (!(await shareHasItem(share.id, itemId))) return new Response('Nicht gefunden.', { status: 404 }); const item = await one( 'SELECT result_path, thumb_path, filename, has_alpha FROM items WHERE id=$1', [itemId]); if (!item?.result_path) return new Response('Nicht gefunden.', { status: 404 }); const download = q.get('download') === '1'; const wantThumb = q.get('thumb') === '1'; // Wer den Download abgeschaltet hat, gibt auch die Originaldatei nicht heraus — // sonst wäre die Einstellung nur Zierde. if (download && !share.allow_download) return new Response('Herunterladen ist für diesen Link aus.', { status: 403 }); const path = wantThumb ? (item.thumb_path || item.result_path) : item.result_path; try { let buf = await getObject(path); const sniff = (b: Buffer): string => b.length > 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff ? 'image/jpeg' : b.length > 12 && b.toString('ascii', 8, 12) === 'WEBP' ? 'image/webp' : 'image/png'; let contentType = path.endsWith('.webp') ? 'image/webp' : sniff(buf); // Ansichtsfassung: kleiner und schneller, vor allem am Handy. Der Download // liefert weiterhin die unveränderte Originaldatei in voller Auflösung. if (!download && !wantThumb && !item.has_alpha) { try { buf = await sharp(buf, { failOn: 'none' }) .resize({ width: 2000, height: 2000, fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 88, mozjpeg: true }).toBuffer(); contentType = 'image/jpeg'; } catch { /* dann eben das Original */ } } if (download) { await trackShare(share.id, 'download', clientAddress, request.headers.get('user-agent')); } const headers: Record = { 'Content-Type': contentType, 'Content-Length': String(buf.length), // Nicht in Suchmaschinen und nicht in fremde Zwischenspeicher. 'X-Robots-Tag': 'noindex, nofollow, noarchive, noimageindex', 'Cache-Control': 'private, max-age=300', }; if (download) { const name = (item.filename || 'klarbild.jpg').replace(/[^\w.\-]/g, '_'); headers['Content-Disposition'] = `attachment; filename="${name}"`; } return new Response(new Uint8Array(buf), { headers }); } catch { return new Response('Bild nicht mehr vorhanden.', { status: 404 }); } };