feat(activities): attach voice and files on activity notes
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -352,7 +352,7 @@ export class ActivitiesController {
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 2 * 1024 * 1024 },
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
addImage(
|
||||
|
||||
@@ -105,6 +105,46 @@ type ActivityRow = {
|
||||
const ACTIVITY_IMAGE_MAX_BYTES = 200 * 1024;
|
||||
const ACTIVITY_IMAGE_MAX_COUNT = 8;
|
||||
const ACTIVITY_IMAGE_MIME = new Set(['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']);
|
||||
const ACTIVITY_VOICE_MAX_BYTES = 2 * 1024 * 1024;
|
||||
const ACTIVITY_VOICE_MAX_COUNT = 5;
|
||||
const ACTIVITY_VOICE_MIME = new Set(['audio/webm', 'audio/ogg', 'audio/mp4', 'audio/mpeg', 'audio/mp3', 'audio/aac', 'audio/x-m4a', 'audio/m4a', 'audio/wav', 'audio/x-wav']);
|
||||
const ACTIVITY_FILE_MAX_BYTES = 10 * 1024 * 1024;
|
||||
const ACTIVITY_FILE_MAX_COUNT = 8;
|
||||
const ACTIVITY_FILE_MIME = new Set([
|
||||
'application/pdf',
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/zip',
|
||||
'application/x-zip-compressed',
|
||||
]);
|
||||
const ACTIVITY_EXT_MIME: Record<string, string> = {
|
||||
pdf: 'application/pdf',
|
||||
txt: 'text/plain',
|
||||
csv: 'text/csv',
|
||||
doc: 'application/msword',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
ppt: 'application/vnd.ms-powerpoint',
|
||||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
zip: 'application/zip',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
webm: 'audio/webm',
|
||||
ogg: 'audio/ogg',
|
||||
mp3: 'audio/mpeg',
|
||||
m4a: 'audio/mp4',
|
||||
wav: 'audio/wav',
|
||||
};
|
||||
|
||||
const include = {
|
||||
stage: { select: { id: true, kind: true, boardId: true } },
|
||||
@@ -840,19 +880,57 @@ export class ActivitiesService {
|
||||
if (!file?.buffer?.length) {
|
||||
throw new BadRequestException('فایلی ارسال نشده است');
|
||||
}
|
||||
const mimeType = (file.mimetype || '').toLowerCase();
|
||||
if (!ACTIVITY_IMAGE_MIME.has(mimeType)) {
|
||||
throw new BadRequestException('فقط عکس میتوانید اضافه کنید.');
|
||||
const namedExt = path.extname(file.originalname || '').slice(1).toLowerCase();
|
||||
let mimeType = (file.mimetype || '').toLowerCase().split(';')[0]?.trim() || '';
|
||||
if (!mimeType || mimeType === 'application/octet-stream') {
|
||||
mimeType = ACTIVITY_EXT_MIME[namedExt] || mimeType;
|
||||
}
|
||||
if (file.size > ACTIVITY_IMAGE_MAX_BYTES) {
|
||||
throw new BadRequestException('حجم عکس باید زیر 200 کیلوبایت باشد');
|
||||
const isVoice = mimeType.startsWith('audio/') || ACTIVITY_VOICE_MIME.has(mimeType);
|
||||
const isImage = ACTIVITY_IMAGE_MIME.has(mimeType);
|
||||
const isFile = ACTIVITY_FILE_MIME.has(mimeType);
|
||||
if (!isVoice && !isImage && !isFile) {
|
||||
throw new BadRequestException('این نوع فایل مجاز نیست.');
|
||||
}
|
||||
const count = await this.prisma.activityImage.count({ where: { activityId } });
|
||||
if (count >= ACTIVITY_IMAGE_MAX_COUNT) {
|
||||
throw new BadRequestException(`حداکثر ${ACTIVITY_IMAGE_MAX_COUNT} عکس برای هر فعالیت مجاز است`);
|
||||
if (isVoice) {
|
||||
if (file.size > ACTIVITY_VOICE_MAX_BYTES) {
|
||||
throw new BadRequestException('حجم صوت باید زیر ۲ مگابایت باشد');
|
||||
}
|
||||
const voiceCount = await this.prisma.activityImage.count({
|
||||
where: { activityId, mimeType: { startsWith: 'audio/' } },
|
||||
});
|
||||
if (voiceCount >= ACTIVITY_VOICE_MAX_COUNT) {
|
||||
throw new BadRequestException(`حداکثر ${ACTIVITY_VOICE_MAX_COUNT} صوت برای هر فعالیت مجاز است`);
|
||||
}
|
||||
} else if (isImage) {
|
||||
if (file.size > ACTIVITY_IMAGE_MAX_BYTES) {
|
||||
throw new BadRequestException('حجم عکس باید زیر 200 کیلوبایت باشد');
|
||||
}
|
||||
const count = await this.prisma.activityImage.count({
|
||||
where: { activityId, mimeType: { startsWith: 'image/' } },
|
||||
});
|
||||
if (count >= ACTIVITY_IMAGE_MAX_COUNT) {
|
||||
throw new BadRequestException(`حداکثر ${ACTIVITY_IMAGE_MAX_COUNT} عکس برای هر فعالیت مجاز است`);
|
||||
}
|
||||
} else {
|
||||
if (file.size > ACTIVITY_FILE_MAX_BYTES) {
|
||||
throw new BadRequestException('حجم فایل باید زیر ۱۰ مگابایت باشد');
|
||||
}
|
||||
const fileCount = await this.prisma.activityImage.count({
|
||||
where: {
|
||||
activityId,
|
||||
NOT: {
|
||||
OR: [{ mimeType: { startsWith: 'image/' } }, { mimeType: { startsWith: 'audio/' } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
if (fileCount >= ACTIVITY_FILE_MAX_COUNT) {
|
||||
throw new BadRequestException(`حداکثر ${ACTIVITY_FILE_MAX_COUNT} فایل برای هر فعالیت مجاز است`);
|
||||
}
|
||||
}
|
||||
const fileName = sanitizeActivityImageName(file.originalname);
|
||||
const ext = path.extname(fileName).slice(0, 12) || '.jpg';
|
||||
const ext =
|
||||
path.extname(fileName).slice(0, 12) ||
|
||||
(mimeType === 'audio/ogg' ? '.ogg' : mimeType === 'audio/mp4' ? '.m4a' : isVoice ? '.webm' : '.jpg');
|
||||
const imageId = randomUUID().replace(/-/g, '').slice(0, 24);
|
||||
const storageKey = path
|
||||
.join(organizationId, 'activities', activityId, `${imageId}${ext}`)
|
||||
@@ -896,7 +974,7 @@ export class ActivitiesService {
|
||||
const row = await this.prisma.activityImage.findFirst({
|
||||
where: { id: imageId, activityId, organizationId },
|
||||
});
|
||||
if (!row) throw new NotFoundException('عکس پیدا نشد');
|
||||
if (!row) throw new NotFoundException('فایل پیدا نشد');
|
||||
await this.files.remove(row.storageKey);
|
||||
await this.prisma.activityImage.delete({ where: { id: row.id } });
|
||||
await this.storage.addLocal(organizationId, -row.sizeBytes);
|
||||
@@ -912,7 +990,7 @@ export class ActivitiesService {
|
||||
const row = await this.prisma.activityImage.findFirst({
|
||||
where: { id: imageId, activityId, organizationId },
|
||||
});
|
||||
if (!row) throw new NotFoundException('عکس پیدا نشد');
|
||||
if (!row) throw new NotFoundException('فایل پیدا نشد');
|
||||
return {
|
||||
stream: this.files.openReadStream(row.storageKey),
|
||||
fileName: row.fileName,
|
||||
|
||||
Reference in New Issue
Block a user