UNPKG

@andrewlwn77/s3-upload-mcp-server

Version:

Pure Node.js MCP server for uploading images to AWS S3 with high-performance validation using Sharp and file-type

195 lines (162 loc) 5.84 kB
import sharp from 'sharp'; import { fileTypeFromBuffer } from 'file-type'; import { promises as fs } from 'fs'; import { Logger } from '../utils/logger'; import { ImageValidationResult, S3Response } from '../types'; export class ImageValidator { private logger = Logger.getInstance(); private readonly maxFileSize: number; constructor(maxFileSize: number = 10 * 1024 * 1024) { // 10MB default this.maxFileSize = maxFileSize; } async validateImageFile(filePath: string): Promise<S3Response<ImageValidationResult>> { try { // Check if file exists try { await fs.access(filePath); } catch { return { success: true, is_valid: false, validation_errors: ['File does not exist'] }; } // Get file stats and read buffer const stats = await fs.stat(filePath); const fileSize = stats.size; // Check file size if (fileSize > this.maxFileSize) { return { success: true, is_valid: false, file_size: fileSize, validation_errors: [`File size ${fileSize} exceeds maximum allowed size ${this.maxFileSize}`] }; } // Read file buffer for validation const buffer = await fs.readFile(filePath); // Use Node.js validation const result = await this.validateImageBuffer(buffer, fileSize); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown validation error'; this.logger.error('Image validation failed', { error: errorMessage, filePath }); return { success: false, error: { code: 'VALIDATION_ERROR', message: errorMessage } }; } } async validateBase64Image(base64Data: string, filename: string): Promise<S3Response<ImageValidationResult>> { try { // Basic base64 validation const base64Pattern = /^data:image\/[a-z]+;base64,/; const hasDataUrl = base64Pattern.test(base64Data); if (!hasDataUrl && !this.isValidBase64(base64Data)) { return { success: true, is_valid: false, validation_errors: ['Invalid base64 data format'] }; } // Extract base64 data const base64String = base64Data.replace(/^data:image\/[a-z]+;base64,/, ''); const buffer = Buffer.from(base64String, 'base64'); // Check size if (buffer.length > this.maxFileSize) { return { success: true, is_valid: false, file_size: buffer.length, validation_errors: [`File size ${buffer.length} exceeds maximum allowed size ${this.maxFileSize}`] }; } // Use Node.js validation directly on buffer const result = await this.validateImageBuffer(buffer, buffer.length); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown base64 validation error'; this.logger.error('Base64 image validation failed', { error: errorMessage, filename }); return { success: false, error: { code: 'VALIDATION_ERROR', message: errorMessage } }; } } private async validateImageBuffer(buffer: Buffer, fileSize: number): Promise<ImageValidationResult> { try { // Detect MIME type using file-type const fileType = await fileTypeFromBuffer(buffer); const mimeType = fileType?.mime || 'application/octet-stream'; // Define supported image formats const supportedFormats = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp' ]; const validationErrors: string[] = []; // Check if it's a supported image format if (!supportedFormats.includes(mimeType)) { validationErrors.push(`Unsupported file format: ${mimeType}`); } let dimensions: { width: number; height: number } | undefined; let fileFormat: string | undefined; // Use Sharp to extract metadata and validate image integrity try { const metadata = await sharp(buffer).metadata(); dimensions = { width: metadata.width || 0, height: metadata.height || 0 }; fileFormat = metadata.format?.toLowerCase(); // Additional validation - Sharp will throw if image is corrupt await sharp(buffer).stats(); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; validationErrors.push(`Invalid image file: ${errorMessage}`); } const isValid = validationErrors.length === 0; const result: ImageValidationResult = { success: true, is_valid: isValid, file_format: fileFormat, file_size: fileSize, content_type: mimeType }; if (dimensions) { result.dimensions = dimensions; } if (!isValid) { result.validation_errors = validationErrors; } this.logger.info('Image validation completed', { isValid, fileFormat, mimeType, dimensions, fileSize }); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown validation error'; this.logger.error('Image buffer validation failed', { error: errorMessage }); return { success: true, is_valid: false, validation_errors: [`Image validation error: ${errorMessage}`] }; } } private isValidBase64(str: string): boolean { try { return btoa(atob(str)) === str; } catch { return false; } } }