feat(03-02): add uploadImage form action with thumbnail generation

- Add uploadImage action that handles multipart file uploads
- Validate file type and entry existence before processing
- Generate thumbnail using Sharp with EXIF rotation
- Save original and thumbnail to filesystem
- Store image metadata in database
- Return entries with their images attached from load function
This commit is contained in:
Thomas Richter
2026-01-29 15:27:12 +01:00
parent e442b9ef0c
commit de3aa5ac4e

View File

@@ -1,13 +1,21 @@
import type { PageServerLoad, Actions } from './$types'; import type { PageServerLoad, Actions } from './$types';
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
import { entryRepository } from '$lib/server/db/repository'; import { entryRepository, imageRepository } from '$lib/server/db/repository';
import { saveOriginal, saveThumbnail, ensureDirectories } from '$lib/server/images/storage';
import { generateThumbnail } from '$lib/server/images/thumbnails';
export const load: PageServerLoad = async ({ url }) => { export const load: PageServerLoad = async ({ url }) => {
const showCompleted = url.searchParams.get('showCompleted') === 'true'; const showCompleted = url.searchParams.get('showCompleted') === 'true';
const entries = entryRepository.getOrdered({ showCompleted }); const entries = entryRepository.getOrdered({ showCompleted });
// Attach images to each entry
const entriesWithImages = entries.map((entry) => ({
...entry,
images: imageRepository.getByEntryId(entry.id)
}));
return { return {
entries, entries: entriesWithImages,
showCompleted showCompleted
}; };
}; };
@@ -127,5 +135,58 @@ export const actions: Actions = {
entryRepository.update(id, { status: newStatus }); entryRepository.update(id, { status: newStatus });
return { success: true }; return { success: true };
},
uploadImage: async ({ request }) => {
const formData = await request.formData();
const file = formData.get('image') as File;
const entryId = formData.get('entryId') as string;
if (!file || file.size === 0) {
return fail(400, { error: 'No file uploaded' });
}
if (!entryId) {
return fail(400, { error: 'Entry ID required' });
}
// Verify entry exists
const entry = entryRepository.getById(entryId);
if (!entry) {
return fail(404, { error: 'Entry not found' });
}
// Validate image type
if (!file.type.startsWith('image/')) {
return fail(400, { error: 'File must be an image' });
}
const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg';
const buffer = Buffer.from(await file.arrayBuffer());
try {
await ensureDirectories();
// Generate and save thumbnail
const thumbnailBuffer = await generateThumbnail(buffer);
// Save to database to get ID
const image = imageRepository.create({
entryId,
filename: file.name,
ext
});
// Save original
await saveOriginal(image.id, ext, buffer);
// Save thumbnail
await saveThumbnail(image.id, thumbnailBuffer);
return { success: true, imageId: image.id };
} catch (err) {
console.error('Upload error:', err);
return fail(500, { error: 'Failed to save image' });
}
} }
}; };