import type { HttpContext } from '@adonisjs/core/http' import drive from '@adonisjs/drive/services/main' import logger from '@adonisjs/core/services/logger' import { randomUUID } from 'node:crypto' import { extname } from 'node:path' import { Jimp } from 'jimp' import fs from 'node:fs/promises' const allowedExtnames = ['jpg', 'jpeg', 'png', 'webp', 'gif'] const imageMimeByExt: Record = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif', } export default class ImagesController { async upload({ request, response }: HttpContext) { const image = request.file('image', { size: '10mb', extnames: allowedExtnames, }) if (!image) { return response.badRequest({ message: 'Image file is required' }) } if (!image.isValid) { const message = image.errors.map((error) => error.message).join(', ') return response.badRequest({ message: message || 'Invalid image file' }) } const extension = image.extname || 'bin' const filename = `${randomUUID()}.${extension}` await drive.use().put(filename, await fs.readFile(image.tmpPath!)) // Generate thumbnail try { const jimpImage = await Jimp.read(image.tmpPath!) const buffer = await jimpImage .resize({ w: 200 }) // Resize to width 200px, maintain aspect ratio .getBuffer('image/jpeg') await drive.use().put(`thumbnails/${filename}`, buffer) } catch (error) { logger.error('Thumbnail generation failed: %s', error.message) // We don't fail the upload if thumbnail generation fails, but we log it } return response.ok({ filename, url: `/api/images/${filename}`, thumbnailUrl: `/api/images/${filename}?type=thumbnail`, }) } async show({ params, request, response }: HttpContext) { const filename = String(params.filename || '') const type = request.input('type') if (!filename || filename.includes('/') || filename.includes('\\')) { return response.badRequest({ message: 'Invalid image name' }) } let key = filename if (type === 'thumbnail') { const thumbnailKey = `thumbnails/${filename}` if (await drive.use().exists(thumbnailKey)) { key = thumbnailKey } } if (!(await drive.use().exists(key))) { return response.notFound({ message: 'Image not found' }) } const mimeType = imageMimeByExt[extname(filename).toLowerCase()] || 'application/octet-stream' response.header('Content-Type', mimeType) response.header('Cache-Control', 'public, max-age=86400') return response.stream(await drive.use().getStream(key)) } async list() { const { objects } = await drive.use().listAll('') const images = [] for (const file of objects) { if (file.isFile) { images.push({ filename: file.key, url: `/api/images/${file.key}`, thumbnailUrl: `/api/images/${file.key}?type=thumbnail`, }) } } return images } async delete() { } }