@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
text/typescript
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;
}
}
}