Feature: user upload artwork
This commit is contained in:
206
server/src/routes/Artworks.ts
Normal file
206
server/src/routes/Artworks.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user