Add backend server with API endpoints
This commit is contained in:
157
backend/src/server.js
Normal file
157
backend/src/server.js
Normal file
@@ -0,0 +1,157 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const multer = require('multer');
|
||||
const sharp = require('sharp');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const MAX_FILE_SIZE = (process.env.MAX_FILE_SIZE || 10) * 1024 * 1024;
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Serve static frontend
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// Configure multer for memory storage
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: MAX_FILE_SIZE },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype === 'image/png') {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only PNG files are allowed'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Process image endpoint
|
||||
app.post('/api/process', upload.single('image'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No image file provided' });
|
||||
}
|
||||
|
||||
const { width, height, quality, maintainAspectRatio } = req.body;
|
||||
|
||||
// Parse dimensions
|
||||
const targetWidth = width ? parseInt(width) : null;
|
||||
const targetHeight = height ? parseInt(height) : null;
|
||||
const compressionQuality = quality ? parseInt(quality) : 80;
|
||||
|
||||
// Validate inputs
|
||||
if (targetWidth && (targetWidth < 1 || targetWidth > 10000)) {
|
||||
return res.status(400).json({ error: 'Width must be between 1 and 10000' });
|
||||
}
|
||||
if (targetHeight && (targetHeight < 1 || targetHeight > 10000)) {
|
||||
return res.status(400).json({ error: 'Height must be between 1 and 10000' });
|
||||
}
|
||||
if (compressionQuality < 1 || compressionQuality > 100) {
|
||||
return res.status(400).json({ error: 'Quality must be between 1 and 100' });
|
||||
}
|
||||
|
||||
// Process image with Sharp
|
||||
let pipeline = sharp(req.file.buffer);
|
||||
|
||||
// Resize if dimensions provided
|
||||
if (targetWidth || targetHeight) {
|
||||
const resizeOptions = {
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
fit: maintainAspectRatio === 'true' ? 'inside' : 'fill'
|
||||
};
|
||||
pipeline = pipeline.resize(resizeOptions);
|
||||
}
|
||||
|
||||
// Apply PNG compression
|
||||
pipeline = pipeline.png({
|
||||
quality: compressionQuality,
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: true
|
||||
});
|
||||
|
||||
// Convert to buffer
|
||||
const processedBuffer = await pipeline.toBuffer();
|
||||
|
||||
// Get metadata for response
|
||||
const metadata = await sharp(processedBuffer).metadata();
|
||||
|
||||
// Send processed image
|
||||
res.set({
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': processedBuffer.length,
|
||||
'Content-Disposition': `attachment; filename="processed-${Date.now()}.png"`,
|
||||
'X-Image-Width': metadata.width,
|
||||
'X-Image-Height': metadata.height,
|
||||
'X-Original-Size': req.file.size,
|
||||
'X-Processed-Size': processedBuffer.length
|
||||
});
|
||||
|
||||
res.send(processedBuffer);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Image processing error:', error);
|
||||
res.status(500).json({ error: 'Failed to process image', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get image metadata endpoint
|
||||
app.post('/api/metadata', upload.single('image'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No image file provided' });
|
||||
}
|
||||
|
||||
const metadata = await sharp(req.file.buffer).metadata();
|
||||
|
||||
res.json({
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
format: metadata.format,
|
||||
size: req.file.size,
|
||||
hasAlpha: metadata.hasAlpha,
|
||||
channels: metadata.channels
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Metadata extraction error:', error);
|
||||
res.status(500).json({ error: 'Failed to extract metadata', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({ error: `File too large. Max size is ${MAX_FILE_SIZE / 1024 / 1024}MB` });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
|
||||
console.error('Server error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// Serve frontend for all other routes
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`PNGer server running on port ${PORT}`);
|
||||
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`Max file size: ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
||||
});
|
||||
Reference in New Issue
Block a user