import { json, error } from '@sveltejs/kit'; import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { env } from '$env/dynamic/private'; export async function GET({ url }) { const nextToken = url.searchParams.get('next') || undefined; try { const s3 = new S3Client({ endpoint: env.B2_ENDPOINT, region: env.B2_REGION, credentials: { accessKeyId: env.B2_ACCESS_KEY_ID, secretAccessKey: env.B2_SECRET_ACCESS_KEY } }); const tokenDuration = parseInt(env.B2_TOKEN_DURATION, 10) || 600; // Fetch a batch of files from the thumbs/ folder const listCommand = new ListObjectsV2Command({ Bucket: env.B2_BUCKET_NAME, Prefix: 'thumbs/', MaxKeys: 40, ContinuationToken: nextToken }); const listResponse = await s3.send(listCommand); const contents = listResponse.Contents || []; // Strict Filter: Only allow actual .jpg or .jpeg extensions const imageFiles = contents.filter(file => { const lowerKey = file.Key.toLowerCase(); return lowerKey.endsWith('.jpg') || lowerKey.endsWith('.jpeg'); }); // Generate presigned URLs for validated items const photos = await Promise.all( imageFiles.map(async (file) => { const id = file.Key.replace('thumbs/', ''); const fullSizeKey = `images/${id}`; const thumbCommand = new GetObjectCommand({ Bucket: env.B2_BUCKET_NAME, Key: file.Key }); const fullCommand = new GetObjectCommand({ Bucket: env.B2_BUCKET_NAME, Key: fullSizeKey }); // 10 minutes = 600 seconds const [thumbUrl, fullUrl] = await Promise.all([ getSignedUrl(s3, thumbCommand, { expiresIn: tokenDuration }), getSignedUrl(s3, fullCommand, { expiresIn: tokenDuration }) ]); return { id, thumbUrl, fullUrl }; }) ); return json({ photos, nextContinuationToken: listResponse.NextContinuationToken || null }); } catch (err) { console.error(err); throw error(500, 'Failed to process images from storage.'); } }