From 74d051cbfe06047535c2893bf00f01687a7e658e Mon Sep 17 00:00:00 2001 From: whidix Date: Mon, 2 Mar 2026 22:25:33 +0100 Subject: [PATCH] feat: implement file upload functionality and create file serving endpoint --- .gitignore | 5 +- .../(admin)/admin/characters/+page.server.ts | 35 ++++++++++++- .../(admin)/admin/characters/+page.svelte | 25 ++++++++- src/routes/uploads/[filename]/+server.ts | 51 +++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/routes/uploads/[filename]/+server.ts diff --git a/.gitignore b/.gitignore index b03f683..0e22363 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,7 @@ vite.config.ts.timestamp-* *.db # Script outputs -/scraped-data \ No newline at end of file +/scraped-data + +# Uploads +/uploads diff --git a/src/routes/(admin)/admin/characters/+page.server.ts b/src/routes/(admin)/admin/characters/+page.server.ts index d3cf1da..dc0b5c3 100644 --- a/src/routes/(admin)/admin/characters/+page.server.ts +++ b/src/routes/(admin)/admin/characters/+page.server.ts @@ -3,6 +3,9 @@ import { character, devilFruit, arc, characterOverride } from '$lib/server/db/sc import { eq } from 'drizzle-orm'; import { fail } from '@sveltejs/kit'; import type { PageServerLoad, Actions } from './$types'; +import { writeFile } from 'fs/promises'; +import { join } from 'path'; +import { existsSync, mkdirSync } from 'fs'; export const load: PageServerLoad = async () => { const [charactersData, devilFruits, arcs, overrides] = await Promise.all([ @@ -101,8 +104,38 @@ export const actions: Actions = { const updates: Record = {}; + // Handle file upload + const pictureFile = formData.get('pictureFile') as File; + const hasUploadedPicture = !!pictureFile && pictureFile.size > 0; + if (hasUploadedPicture) { + try { + const uploadsDir = join(process.cwd(), 'uploads'); + if (!existsSync(uploadsDir)) { + mkdirSync(uploadsDir, { recursive: true }); + } + + // Get file extension + const extension = pictureFile.name.split('.').pop(); + const filename = `${id}.${extension}`; + const filepath = join(uploadsDir, filename); + + // Convert file to buffer and save + const buffer = Buffer.from(await pictureFile.arrayBuffer()); + await writeFile(filepath, buffer); + + // Update pictureUrl to point to the handler route + updates.pictureUrl = `/uploads/${filename}`; + } catch (error) { + console.error('File upload error:', error); + return fail(500, { error: 'Failed to upload file' }); + } + } + formData.forEach((value, key) => { - if (key !== 'id') { + if (key !== 'id' && key !== 'pictureFile') { + if (hasUploadedPicture && key === 'pictureUrl') { + return; + } // Handle integers (age, bounty, height, devilFruitId, arcId) if (key === 'age' || key === 'bounty' || key === 'height' || key === 'devilFruitId' || key === 'arcId') { const strValue = value as string; diff --git a/src/routes/(admin)/admin/characters/+page.svelte b/src/routes/(admin)/admin/characters/+page.svelte index 381bdeb..d17cd29 100644 --- a/src/routes/(admin)/admin/characters/+page.svelte +++ b/src/routes/(admin)/admin/characters/+page.svelte @@ -30,6 +30,14 @@ }, 3000); }; + const handleFileSelect = (event: Event) => { + const target = event.target as HTMLInputElement; + if (target.files && target.files.length > 0) { + // Clear pictureUrl when a file is selected + editForm.pictureUrl = ''; + } + }; + const getFandomUrl = (url: string | null | undefined) => { if (!url) return null; if (url.startsWith('http://') || url.startsWith('https://')) return url; @@ -516,6 +524,7 @@ class="mt-6 space-y-4" method="POST" action="?/update" + enctype="multipart/form-data" use:enhance={() => { isSaving = true; return async ({ result }) => { @@ -776,10 +785,24 @@ name="pictureUrl" bind:value={editForm.pictureUrl} placeholder={selectedChar?.pictureUrl || ''} - class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40" + class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40" /> +
+ + +

File will be saved as {selectedChar?.id}.jpg/png/etc

+
+ +
{ + const filename = params.filename as string; + + if (!filename || typeof filename !== 'string' || filename.includes('..') || filename.includes('/')) { + return new Response('Invalid filename', { status: 400 }); + } + + try { + const uploadsDir = join(process.cwd(), 'uploads'); + const filepath = join(uploadsDir, filename); + + if (!existsSync(filepath)) { + return new Response('File not found', { status: 404 }); + } + + // Verify the file is within uploads directory (prevent directory traversal) + if (!filepath.startsWith(uploadsDir)) { + return new Response('Access denied', { status: 403 }); + } + + const fileBuffer = await readFile(filepath); + + // Determine content type based on file extension + const ext = (filename as string).split('.').pop()?.toLowerCase() || ''; + const contentTypes: Record = { + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'gif': 'image/gif', + 'webp': 'image/webp', + 'svg': 'image/svg+xml' + }; + const contentType = contentTypes[ext] || 'application/octet-stream'; + + return new Response(fileBuffer, { + status: 200, + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=86400' + } + }); + } catch (error) { + console.error('File serving error:', error); + return new Response('Internal server error', { status: 500 }); + } +};