Image upload functionality

This commit is contained in:
2024-12-01 16:49:31 +00:00
parent bacb955108
commit bd93641af1
2 changed files with 137 additions and 33 deletions

View File

@@ -1,33 +1,135 @@
import { createWriteStream } from 'fs'; // File: src/routes/images.js
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
import { createWriteStream } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { db } from '../db.js'; import { db } from '../db.js';
export default async function images(fastify, options) { const __filename = fileURLToPath(import.meta.url);
fastify.addHook('preHandler', async (request, reply) => { const __dirname = dirname(__filename);
try {
await request.jwtVerify(); // Auth middleware helper
} catch (err) { const authenticate = async (request, reply) => {
reply.send(err); try {
await request.jwtVerify();
} catch (err) {
reply.code(401).send({ error: 'Unauthorized' });
}
};
export default async function imageRoutes(fastify) {
// Upload image for a post
fastify.post('/api/posts/:postId/images', {
onRequest: [authenticate],
handler: async (request, reply) => {
const { postId } = request.params;
try {
// Verify post exists and belongs to user
const post = await db.oneOrNone(
'SELECT id FROM posts WHERE id = $1 AND author_id = $2',
[postId, request.user.id]
);
if (!post) {
reply.code(404).send({ error: 'Post not found' });
return;
}
// Handle file upload
const data = await request.file();
if (!data) {
reply.code(400).send({ error: 'No file uploaded' });
return;
}
// Validate file type
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedMimeTypes.includes(data.mimetype)) {
reply.code(400).send({ error: 'Invalid file type' });
return;
}
// Create unique filename
const fileExt = data.filename.split('.').pop();
const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${fileExt}`;
const uploadDir = join(__dirname, '../../uploads');
const filepath = join(uploadDir, filename);
// Save file to disk
await pipeline(data.file, createWriteStream(filepath));
// Save image record in database
const image = await db.one(
`INSERT INTO images (filename, url, post_id)
VALUES ($1, $2, $3)
RETURNING *`,
[filename, `/uploads/${filename}`, postId]
);
reply.code(201).send(image);
} catch (err) {
fastify.log.error(err);
reply.code(500).send({ error: 'Internal server error' });
}
} }
}); });
fastify.post('/api/images/:postId', async (request, reply) => { // Delete image
const { postId } = request.params; fastify.delete('/api/images/:id', {
const data = await request.file(); onRequest: [authenticate],
handler: async (request, reply) => {
const filename = `${Date.now()}-${data.filename}`; const { id } = request.params;
const path = join(__dirname, '../../uploads', filename);
await pipeline(data.file, createWriteStream(path));
const image = await db.one(
`INSERT INTO images (filename, url, post_id)
VALUES ($1, $2, $3)
RETURNING *`,
[filename, `/uploads/${filename}`, postId]
);
reply.send(image); try {
const image = await db.oneOrNone(
`SELECT i.* FROM images i
JOIN posts p ON i.post_id = p.id
WHERE i.id = $1 AND p.author_id = $2`,
[id, request.user.id]
);
if (!image) {
reply.code(404).send({ error: 'Image not found' });
return;
}
// Delete from database
await db.none('DELETE FROM images WHERE id = $1', [id]);
// We could also delete the file from disk here, but we might want to keep it
// for backup purposes or to handle undo operations
reply.code(204).send();
} catch (err) {
fastify.log.error(err);
reply.code(500).send({ error: 'Internal server error' });
}
}
});
// Get images for a post
fastify.get('/api/posts/:postId/images', {
onRequest: [authenticate],
handler: async (request, reply) => {
const { postId } = request.params;
try {
const images = await db.any(
`SELECT i.* FROM images i
JOIN posts p ON i.post_id = p.id
WHERE p.id = $1 AND p.author_id = $2
ORDER BY i.uploaded_at DESC`,
[postId, request.user.id]
);
reply.send(images);
} catch (err) {
fastify.log.error(err);
reply.code(500).send({ error: 'Internal server error' });
}
}
}); });
} }

View File

@@ -9,6 +9,7 @@ import { dirname } from 'path';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import authRoutes from './routes/auth.js'; import authRoutes from './routes/auth.js';
import postRoutes from './routes/posts.js'; import postRoutes from './routes/posts.js';
import imageRoutes from './routes/images.js';
// Load environment variables // Load environment variables
dotenv.config(); dotenv.config();
@@ -17,15 +18,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const app = Fastify({ const app = Fastify({
logger: true, logger: true
ajv: {
customOptions: {
removeAdditional: false,
useDefaults: true,
coerceTypes: true,
allErrors: true
}
}
}); });
// Register plugins // Register plugins
@@ -37,16 +30,25 @@ await app.register(jwt, {
secret: process.env.JWT_SECRET || 'your-super-secret-key-change-this-in-production' secret: process.env.JWT_SECRET || 'your-super-secret-key-change-this-in-production'
}); });
await app.register(multipart); await app.register(multipart, {
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
}
});
await app.register(fastifyStatic, { await app.register(fastifyStatic, {
root: join(__dirname, '../uploads'), root: join(__dirname, '../uploads'),
prefix: '/uploads/' prefix: '/uploads/'
}); });
// Create uploads directory if it doesn't exist
import { mkdirSync } from 'fs';
mkdirSync(join(__dirname, '../uploads'), { recursive: true });
// Register routes // Register routes
await app.register(authRoutes); await app.register(authRoutes);
await app.register(postRoutes); await app.register(postRoutes);
await app.register(imageRoutes);
// Start the server // Start the server
const start = async () => { const start = async () => {