88 lines
2.3 KiB
TypeScript
88 lines
2.3 KiB
TypeScript
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<void> {
|
|
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);
|
|
}
|
|
}
|