From 642359fec99a40190a35b9609da0a2094a8880cd Mon Sep 17 00:00:00 2001 From: grabowski Date: Tue, 7 Apr 2026 17:16:02 +0700 Subject: [PATCH] Fix image uploads: accept more formats, convert to JPEG, show errors - Added HEIF, GIF, AVIF, BMP, TIFF to allowed image types - All uploads converted to JPEG via sharp (fixes HEIC/HEIF from iPhones) - Upload action wrapped in try/catch, errors shown in UI - Error banner displayed above image section on failure Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/server/uploads.ts | 15 ++++++++----- src/routes/(app)/devices/[id]/+page.server.ts | 22 +++++++++++-------- src/routes/(app)/devices/[id]/+page.svelte | 6 ++++- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/lib/server/uploads.ts b/src/lib/server/uploads.ts index e84adbe..5775b91 100644 --- a/src/lib/server/uploads.ts +++ b/src/lib/server/uploads.ts @@ -6,7 +6,10 @@ import sharp from 'sharp'; const UPLOAD_BASE = 'static/uploads'; const THUMBNAIL_WIDTH = 300; -const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/heic']; +const ALLOWED_IMAGE_TYPES = [ + 'image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif', + 'image/bmp', 'image/tiff', 'image/gif', 'image/avif' +]; const ALLOWED_DOC_TYPES = ['application/pdf', 'text/plain', 'application/zip']; export async function saveImage( @@ -14,10 +17,10 @@ export async function saveImage( subfolder: 'devices' | 'components' ): Promise<{ filePath: string; thumbnailPath: string }> { if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { - throw new Error(`Invalid image type: ${file.type}`); + throw new Error(`Invalid image type: ${file.type}. Allowed: JPEG, PNG, WebP, HEIC, GIF, AVIF, BMP, TIFF`); } - const ext = extname(file.name) || '.jpg'; + const ext = '.jpg'; // always save as jpg for consistency const filename = `${randomUUID()}${ext}`; const thumbFilename = `thumb_${filename}`; @@ -26,15 +29,15 @@ export async function saveImage( const buffer = Buffer.from(await file.arrayBuffer()); - // Save original + // Convert to JPEG and save original const filePath = join(dir, filename); - await writeFile(filePath, buffer); + await sharp(buffer).jpeg({ quality: 90 }).toFile(filePath); // Generate thumbnail const thumbnailPath = join(dir, thumbFilename); await sharp(buffer).resize(THUMBNAIL_WIDTH).jpeg({ quality: 80 }).toFile(thumbnailPath); - // Return paths relative to static/ for serving + // Return URL paths (always forward slashes) return { filePath: `/uploads/${subfolder}/${filename}`, thumbnailPath: `/uploads/${subfolder}/${thumbFilename}` diff --git a/src/routes/(app)/devices/[id]/+page.server.ts b/src/routes/(app)/devices/[id]/+page.server.ts index 6940a1c..bef46df 100644 --- a/src/routes/(app)/devices/[id]/+page.server.ts +++ b/src/routes/(app)/devices/[id]/+page.server.ts @@ -217,17 +217,21 @@ export const actions: Actions = { const file = formData.get('image') as File; if (!file || file.size === 0) return fail(400, { error: 'No file selected' }); - const { filePath, thumbnailPath } = await saveImage(file, 'devices'); - const caption = formData.get('caption') as string; + try { + const { filePath, thumbnailPath } = await saveImage(file, 'devices'); + const caption = formData.get('caption') as string; - await db.insert(deviceImages).values({ - deviceId: params.id, - filePath, - thumbnailPath, - caption: caption || null - }); + await db.insert(deviceImages).values({ + deviceId: params.id, + filePath, + thumbnailPath, + caption: caption || null + }); - return { imageUploaded: true }; + return { imageUploaded: true }; + } catch (err) { + return fail(400, { error: `Upload failed: ${err instanceof Error ? err.message : 'Unknown error'}` }); + } }, deleteImage: async ({ request }) => { diff --git a/src/routes/(app)/devices/[id]/+page.svelte b/src/routes/(app)/devices/[id]/+page.svelte index 466987b..67c34f7 100644 --- a/src/routes/(app)/devices/[id]/+page.svelte +++ b/src/routes/(app)/devices/[id]/+page.svelte @@ -3,7 +3,7 @@ import { DEVICE_CONDITIONS, DEVICE_LOG_TYPES } from '$lib/constants.js'; import { formatDate, timeAgo } from '$lib/utils/date.js'; - let { data } = $props(); + let { data, form } = $props(); let showStatusForm = $state(false); let showUploadForm = $state(false); let showDocForm = $state(false); @@ -130,6 +130,10 @@ + {#if form?.error} +
{form.error}
+ {/if} + {#if showUploadForm}