108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
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<string, string> = {
|
|
'.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() {
|
|
|
|
}
|
|
}
|