import sharp from 'sharp'; import { writeFile, mkdir } from 'fs/promises'; import path from 'path'; import crypto from 'crypto'; import { env } from '../config/env.js'; export class MediaService { private allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp']; private maxFileSize: number; constructor() { this.maxFileSize = env.MAX_FILE_SIZE; } /** * Validate file type and size */ async validateFile(file: any): Promise { if (!this.allowedMimeTypes.includes(file.mimetype)) { throw new Error(`Invalid file type. Allowed types: ${this.allowedMimeTypes.join(', ')}`); } // Check file size const buffer = await file.toBuffer(); if (buffer.length > this.maxFileSize) { throw new Error(`File too large. Maximum size: ${this.maxFileSize / 1024 / 1024}MB`); } } /** * Generate safe filename */ generateFilename(originalName: string): string { const ext = path.extname(originalName); const hash = crypto.randomBytes(8).toString('hex'); const timestamp = Date.now(); return `${timestamp}-${hash}${ext}`; } /** * Optimize and save image */ async processAndSaveImage( file: any, destinationDir: string ): Promise<{ filename: string; url: string }> { await this.validateFile(file); // Ensure destination directory exists await mkdir(destinationDir, { recursive: true }); // Generate filename const filename = this.generateFilename(file.filename); const filepath = path.join(destinationDir, filename); // Get file buffer const buffer = await file.toBuffer(); // Process image with sharp (optimize and resize if needed) await sharp(buffer) .resize(2000, 2000, { fit: 'inside', withoutEnlargement: true, }) .jpeg({ quality: 85 }) .png({ quality: 85 }) .webp({ quality: 85 }) .toFile(filepath); // Return filename and URL path return { filename, url: `/images/${filename}`, }; } /** * Save image to git workspace */ async saveToGitWorkspace( file: any, workspaceDir: string ): Promise<{ filename: string; url: string }> { const imagesDir = path.join(workspaceDir, 'public', 'images'); return this.processAndSaveImage(file, imagesDir); } }