import { Readable } from "node:stream"; import { Injectable, InternalServerErrorException, Logger, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import * as NodeClam from "clamscan"; import type { IMediaService, MediaProcessingResult, ScanResult, } from "../common/interfaces/media.interface"; import { ImageProcessorStrategy } from "./strategies/image-processor.strategy"; import { VideoProcessorStrategy } from "./strategies/video-processor.strategy"; interface ClamScanner { scanStream( stream: Readable, ): Promise<{ isInfected: boolean; viruses: string[] }>; } @Injectable() export class MediaService implements IMediaService { private readonly logger = new Logger(MediaService.name); private clamscan: ClamScanner | null = null; private isClamAvInitialized = false; constructor( private readonly configService: ConfigService, private readonly imageProcessor: ImageProcessorStrategy, private readonly videoProcessor: VideoProcessorStrategy, ) { this.initClamScan(); } private async initClamScan() { try { const scanner = await new NodeClam().init({ clamdscan: { host: this.configService.get("CLAMAV_HOST", "localhost"), port: this.configService.get("CLAMAV_PORT", 3310), timeout: 60000, }, preference: "clamdscan", }); this.clamscan = scanner; this.isClamAvInitialized = true; this.logger.log("ClamAV scanner initialized successfully"); } catch (error) { this.logger.warn( `ClamAV scanner could not be initialized: ${error.message}. Antivirus scanning will be skipped.`, ); } } async scanFile(buffer: Buffer, filename: string): Promise { if (!this.isClamAvInitialized || !this.clamscan) { this.logger.warn("ClamAV not initialized, skipping scan"); return { isInfected: false }; } try { const stream = Readable.from(buffer); const { isInfected, viruses } = await this.clamscan.scanStream(stream); if (isInfected) { this.logger.error( `Virus detected in file ${filename}: ${viruses.join(", ")}`, ); } return { isInfected, virusName: isInfected ? viruses[0] : undefined, }; } catch (error) { this.logger.error(`Error scanning file ${filename}: ${error.message}`); throw new InternalServerErrorException("Error during virus scan"); } } async processImage( buffer: Buffer, format: "webp" | "avif" = "webp", ): Promise { return this.imageProcessor.process(buffer, { format }); } async processVideo( buffer: Buffer, format: "webm" | "av1" = "webm", ): Promise { return this.videoProcessor.process(buffer, { format }); } }