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.
97 lines
2.6 KiB
TypeScript
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 });
|
|
}
|
|
}
|