Fix image uploads: accept more formats, convert to JPEG, show errors
Deploy to LXC / deploy (push) Successful in 18s

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 17:16:02 +07:00
parent 0113803378
commit 642359fec9
3 changed files with 27 additions and 16 deletions
+9 -6
View File
@@ -6,7 +6,10 @@ import sharp from 'sharp';
const UPLOAD_BASE = 'static/uploads'; const UPLOAD_BASE = 'static/uploads';
const THUMBNAIL_WIDTH = 300; 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']; const ALLOWED_DOC_TYPES = ['application/pdf', 'text/plain', 'application/zip'];
export async function saveImage( export async function saveImage(
@@ -14,10 +17,10 @@ export async function saveImage(
subfolder: 'devices' | 'components' subfolder: 'devices' | 'components'
): Promise<{ filePath: string; thumbnailPath: string }> { ): Promise<{ filePath: string; thumbnailPath: string }> {
if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { 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 filename = `${randomUUID()}${ext}`;
const thumbFilename = `thumb_${filename}`; const thumbFilename = `thumb_${filename}`;
@@ -26,15 +29,15 @@ export async function saveImage(
const buffer = Buffer.from(await file.arrayBuffer()); const buffer = Buffer.from(await file.arrayBuffer());
// Save original // Convert to JPEG and save original
const filePath = join(dir, filename); const filePath = join(dir, filename);
await writeFile(filePath, buffer); await sharp(buffer).jpeg({ quality: 90 }).toFile(filePath);
// Generate thumbnail // Generate thumbnail
const thumbnailPath = join(dir, thumbFilename); const thumbnailPath = join(dir, thumbFilename);
await sharp(buffer).resize(THUMBNAIL_WIDTH).jpeg({ quality: 80 }).toFile(thumbnailPath); 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 { return {
filePath: `/uploads/${subfolder}/${filename}`, filePath: `/uploads/${subfolder}/${filename}`,
thumbnailPath: `/uploads/${subfolder}/${thumbFilename}` thumbnailPath: `/uploads/${subfolder}/${thumbFilename}`
@@ -217,6 +217,7 @@ export const actions: Actions = {
const file = formData.get('image') as File; const file = formData.get('image') as File;
if (!file || file.size === 0) return fail(400, { error: 'No file selected' }); if (!file || file.size === 0) return fail(400, { error: 'No file selected' });
try {
const { filePath, thumbnailPath } = await saveImage(file, 'devices'); const { filePath, thumbnailPath } = await saveImage(file, 'devices');
const caption = formData.get('caption') as string; const caption = formData.get('caption') as string;
@@ -228,6 +229,9 @@ export const actions: Actions = {
}); });
return { imageUploaded: true }; return { imageUploaded: true };
} catch (err) {
return fail(400, { error: `Upload failed: ${err instanceof Error ? err.message : 'Unknown error'}` });
}
}, },
deleteImage: async ({ request }) => { deleteImage: async ({ request }) => {
+5 -1
View File
@@ -3,7 +3,7 @@
import { DEVICE_CONDITIONS, DEVICE_LOG_TYPES } from '$lib/constants.js'; import { DEVICE_CONDITIONS, DEVICE_LOG_TYPES } from '$lib/constants.js';
import { formatDate, timeAgo } from '$lib/utils/date.js'; import { formatDate, timeAgo } from '$lib/utils/date.js';
let { data } = $props(); let { data, form } = $props();
let showStatusForm = $state(false); let showStatusForm = $state(false);
let showUploadForm = $state(false); let showUploadForm = $state(false);
let showDocForm = $state(false); let showDocForm = $state(false);
@@ -130,6 +130,10 @@
</button> </button>
</div> </div>
{#if form?.error}
<div class="mb-3 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-300">{form.error}</div>
{/if}
{#if showUploadForm} {#if showUploadForm}
<form method="POST" action="?/uploadImage" enctype="multipart/form-data" use:enhance class="mb-4 flex flex-wrap items-end gap-3"> <form method="POST" action="?/uploadImage" enctype="multipart/form-data" use:enhance class="mb-4 flex flex-wrap items-end gap-3">
<input type="file" name="image" accept="image/*" required <input type="file" name="image" accept="image/*" required