feat: implement file upload functionality and create file serving endpoint
All checks were successful
Build Docker Image / build (push) Successful in 1m20s
All checks were successful
Build Docker Image / build (push) Successful in 1m20s
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,3 +27,6 @@ vite.config.ts.timestamp-*
|
||||
|
||||
# Script outputs
|
||||
/scraped-data
|
||||
|
||||
# Uploads
|
||||
/uploads
|
||||
|
||||
@@ -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<string, any> = {};
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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 }) => {
|
||||
@@ -780,6 +789,20 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="char-picture-file" class="block text-sm font-medium text-gray-300 mb-2">Or Upload Picture</label>
|
||||
<input
|
||||
type="file"
|
||||
id="char-picture-file"
|
||||
name="pictureFile"
|
||||
accept="image/*"
|
||||
onchange={handleFileSelect}
|
||||
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-amber-600 file:text-white hover:file:bg-amber-700"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-400">File will be saved as {selectedChar?.id}.jpg/png/etc</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="char-url" class="block text-sm font-medium text-gray-300 mb-2">Fandom URL</label>
|
||||
<input
|
||||
|
||||
51
src/routes/uploads/[filename]/+server.ts
Normal file
51
src/routes/uploads/[filename]/+server.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
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<string, string> = {
|
||||
'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 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user