Files
memegoat/backend/src/media/media.service.ts
Mathis HERRIOT 514bd354bf feat: add modular services and repositories for improved code organization
Introduce repository pattern across multiple services, including `favorites`, `tags`, `sessions`, `reports`, `auth`, and more. Decouple crypto functionalities into modular services like `HashingService`, `JwtService`, and `EncryptionService`. Improve testability and maintainability by simplifying dependencies and consolidating utility logic.
2026-01-14 12:11:39 +01:00

97 lines
2.6 KiB
TypeScript

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<string>("CLAMAV_HOST", "localhost"),
port: this.configService.get<number>("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<ScanResult> {
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<MediaProcessingResult> {
return this.imageProcessor.process(buffer, { format });
}
async processVideo(
buffer: Buffer,
format: "webm" | "av1" = "webm",
): Promise<MediaProcessingResult> {
return this.videoProcessor.process(buffer, { format });
}
}