207 lines
5.5 KiB
TypeScript
207 lines
5.5 KiB
TypeScript
import path from 'path';
|
|
import fs from 'fs';
|
|
import express from 'express';
|
|
import multer from 'multer';
|
|
import {PrismaClient} from '../../generated/prisma';
|
|
|
|
const router = express.Router();
|
|
const prisma = new PrismaClient();
|
|
|
|
// Ensure uploads directory exists
|
|
const uploadsDir = path.resolve('uploads');
|
|
if (!fs.existsSync(uploadsDir)) {
|
|
fs.mkdirSync(uploadsDir, {recursive: true});
|
|
}
|
|
|
|
// Configure multer for file uploads
|
|
const diskStorage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
cb(null, uploadsDir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
// Create unique filename with timestamp
|
|
const uniqueName = `${Date.now()}-${file.originalname}`;
|
|
cb(null, uniqueName);
|
|
}
|
|
});
|
|
|
|
// File validation
|
|
const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
|
|
// Accept only image files
|
|
const allowedMimeTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
|
if (allowedMimeTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP images are allowed.'));
|
|
}
|
|
};
|
|
|
|
const upload = multer({
|
|
storage: diskStorage,
|
|
fileFilter: fileFilter,
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024 // 10MB limit
|
|
}
|
|
});
|
|
|
|
// GET all artworks
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const artworks = await prisma.artworks.findMany({
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
filename: true,
|
|
mimetype: true,
|
|
size: true,
|
|
created_at: true,
|
|
updated_at: true
|
|
},
|
|
orderBy: {
|
|
created_at: 'desc'
|
|
}
|
|
});
|
|
|
|
return res.status(200).json(artworks);
|
|
} catch (error: any) {
|
|
console.error(error.message);
|
|
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
|
}
|
|
});
|
|
|
|
// GET single artwork metadata
|
|
router.get('/:id', async (req, res) => {
|
|
const {id} = req.params;
|
|
try {
|
|
const artwork = await prisma.artworks.findUnique({
|
|
where: {id},
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
filename: true,
|
|
mimetype: true,
|
|
size: true,
|
|
created_at: true,
|
|
updated_at: true
|
|
}
|
|
});
|
|
|
|
if (!artwork) {
|
|
return res.status(404).json({status: 'error', message: 'Artwork not found'});
|
|
}
|
|
|
|
return res.status(200).json(artwork);
|
|
} catch (error: any) {
|
|
console.error(error.message);
|
|
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
|
}
|
|
});
|
|
|
|
// GET artwork image file
|
|
router.get('/:id/image', async (req, res) => {
|
|
const {id} = req.params;
|
|
try {
|
|
const artwork = await prisma.artworks.findUnique({
|
|
where: {id},
|
|
select: {filename: true, mimetype: true}
|
|
});
|
|
|
|
if (!artwork) {
|
|
return res.status(404).json({status: 'error', message: 'Artwork not found'});
|
|
}
|
|
|
|
const filePath = path.join(uploadsDir, artwork.filename);
|
|
|
|
// Check if file exists
|
|
if (!fs.existsSync(filePath)) {
|
|
return res.status(404).json({status: 'error', message: 'Image file not found'});
|
|
}
|
|
|
|
// Set caching headers for better performance
|
|
res.setHeader('Content-Type', artwork.mimetype);
|
|
res.setHeader('Cache-Control', 'public, max-age=31536000'); // Cache for 1 year
|
|
res.setHeader('ETag', artwork.filename); // Use filename as ETag
|
|
|
|
// Send file
|
|
return res.sendFile(filePath);
|
|
} catch (error: any) {
|
|
console.error(error.message);
|
|
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
|
}
|
|
});
|
|
|
|
// POST new artwork
|
|
router.post('/', upload.single('image'), async (req, res) => {
|
|
if (!req.file) {
|
|
return res.status(400).json({status: 'error', message: 'No image file uploaded'});
|
|
}
|
|
|
|
if (!req.body.title) {
|
|
// Cleanup uploaded file if validation fails
|
|
fs.unlinkSync(req.file.path);
|
|
return res.status(400).json({status: 'error', message: 'Title is required'});
|
|
}
|
|
|
|
try {
|
|
const artwork = await prisma.artworks.create({
|
|
data: {
|
|
title: req.body.title,
|
|
filename: req.file.filename,
|
|
mimetype: req.file.mimetype,
|
|
size: req.file.size
|
|
}
|
|
});
|
|
|
|
return res.status(201).json({
|
|
id: artwork.id,
|
|
title: artwork.title,
|
|
filename: artwork.filename,
|
|
mimetype: artwork.mimetype,
|
|
size: artwork.size,
|
|
created_at: artwork.created_at,
|
|
updated_at: artwork.updated_at
|
|
});
|
|
} catch (error: any) {
|
|
// Cleanup uploaded file if database operation fails
|
|
fs.unlinkSync(req.file.path);
|
|
console.error(error.message);
|
|
|
|
if (error.code === 'P2002') {
|
|
return res.status(409).json({status: 'error', message: 'An artwork with this title already exists'});
|
|
}
|
|
|
|
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
|
}
|
|
});
|
|
|
|
// DELETE artwork
|
|
router.delete('/:id', async (req, res) => {
|
|
const {id} = req.params;
|
|
try {
|
|
const artwork = await prisma.artworks.findUnique({
|
|
where: {id},
|
|
select: {filename: true}
|
|
});
|
|
|
|
if (!artwork) {
|
|
return res.status(404).json({status: 'error', message: 'Artwork not found'});
|
|
}
|
|
|
|
// Delete from database
|
|
await prisma.artworks.delete({where: {id}});
|
|
|
|
// Delete file from disk
|
|
const filePath = path.join(uploadsDir, artwork.filename);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
|
|
return res.status(200).json({status: 'success', message: 'Artwork deleted'});
|
|
} catch (error: any) {
|
|
console.error(error.message);
|
|
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
|
}
|
|
});
|
|
|
|
export default router;
|