canina/backend/src/admin/media.service.ts
parsa aghaei 73029c06c4
Some checks failed
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 7s
Deploy Canina / deploy (push) Has been cancelled
fix(media): resolve UTF-8 Persian/Arabic filename encoding issue in uploads
2026-08-26 16:17:08 +03:30

139 lines
3.5 KiB
TypeScript

import { Injectable, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import * as path from 'path';
import * as fs from 'fs';
@Injectable()
export class MediaService {
constructor(private prisma: PrismaService) {}
async getAllMedia() {
const items = await this.prisma.media.findMany({
orderBy: { createdAt: 'desc' },
});
return items.map((item) => {
let cleanFilename = item.filename;
// Check if filename contains common mojibake patterns from latin1 decoding
if (cleanFilename && /[ØÙÚÛ]/.test(cleanFilename)) {
try {
const decoded = Buffer.from(cleanFilename, 'latin1').toString('utf8');
if (decoded && !decoded.includes('')) {
cleanFilename = decoded;
}
} catch {
// Keep original if decoding fails
}
}
return {
...item,
filename: cleanFilename,
};
});
}
async uploadFile(file: Express.Multer.File) {
if (!file) {
throw new BadRequestException('No file uploaded');
}
const uploadDir = path.join(process.cwd(), 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// Fix Multer/Busboy ISO-8859-1 (latin1) to UTF-8 filename encoding for Persian/Arabic/Unicode characters
let originalname = file.originalname;
try {
originalname = Buffer.from(file.originalname, 'latin1').toString('utf8');
} catch {
originalname = file.originalname;
}
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = path.extname(originalname);
const filename = `${uniqueSuffix}${ext}`;
const filePath = path.join(uploadDir, filename);
fs.writeFileSync(filePath, file.buffer);
// Assuming we serve static files from '/uploads' route prefix
const url = `/uploads/${filename}`;
const media = await this.prisma.media.create({
data: {
filename: originalname,
url,
mimetype: file.mimetype,
size: file.size,
},
});
return media;
}
async deleteMedia(id: string) {
const media = await this.prisma.media.findUnique({ where: { id } });
if (!media) throw new BadRequestException('Media not found');
const filePath = path.join(
process.cwd(),
'uploads',
path.basename(media.url),
);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
await this.prisma.media.delete({ where: { id } });
return { success: true };
}
async deleteManyMedia(ids: string[]) {
const mediaItems = await this.prisma.media.findMany({
where: { id: { in: ids } },
});
for (const media of mediaItems) {
const filePath = path.join(
process.cwd(),
'uploads',
path.basename(media.url),
);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
} catch {
// Ignore individual unlink errors
}
}
}
await this.prisma.media.deleteMany({
where: { id: { in: ids } },
});
return { success: true, count: mediaItems.length };
}
async updateMedia(
id: string,
data: {
altText?: string;
title?: string;
description?: string;
caption?: string;
},
) {
return this.prisma.media.update({
where: { id },
data: {
altText: data.altText,
title: data.title,
description: data.description,
caption: data.caption,
},
});
}
}