import { Injectable } from "@nestjs/common"; import { allowedMimes } from "src/modules/attachments/config/upload.config"; import { UploadMetaAttachmentsDto } from "src/modules/attachments/dtos/upload-meta-attachments.dto"; import { Readable } from "node:stream"; import { PrismaService } from "src/prisma/prisma.service"; import { fileTypeFromBuffer } from "file-type"; import { DiskStorageService } from "src/modules/attachments/services/disk-storage.service"; import { VariantsQueue } from "src/modules/attachments/services/variants.queue"; import { Result } from "src/common/errors/result-error.factory"; @Injectable() export class AttachmentUploadService { constructor( private readonly prisma: PrismaService, private readonly disk: DiskStorageService, private readonly variantsQ: VariantsQueue, ) { } async uploadAttachment(file?: Express.Multer.File, meta?: UploadMetaAttachmentsDto): Promise> { if (!file) return { success: false, error: 'FILE_NOT_FOUND' }; //magic detection using binary signature const kind = await fileTypeFromBuffer(file.buffer).catch(() => null); const detected_mime = kind?.mime || file.mimetype || 'application/octet-stream'; //strict whitelist if (!allowedMimes().includes(detected_mime)) { return { success: false, error: 'INVALID_ATTACHMENT_TYPE' }; } //Saving FS (hash + CAS + unDupes) const stream = Readable.from(file.buffer); const { sha256, storage_path, size } = await this.disk.saveStreamAndHash(stream); const now = new Date(); const attachment = await this.prisma.$transaction(async (tx) => { //upsert blob: +1 ref await tx.blobs.upsert({ where: { sha256 }, create: { sha256, storage_path: storage_path, size, mime: detected_mime, refcount: 1, created_at: now, }, update: { //only increment, does not change the storage path refcount: { increment: 1 }, mime: detected_mime, //update mime and size to keep last image size, }, }); const att = await tx.attachments.create({ data: { sha256, owner_type: meta?.owner_type ?? 'EXPENSE', owner_id: meta?.owner_id ?? 'unknown', original_name: file.originalname, status: 'ACTIVE', retention_policy: (meta?.retention_policy ?? 'EXPENSE_7Y') as any, created_by: meta?.created_by ?? 'system', created_at: now, }, }); return att; }); await this.variantsQ.enqueue(attachment.id, detected_mime); return { success: true, data: { ok: true, id: attachment.id, sha256, storage_path: storage_path, size, mime: detected_mime, original_name: file.originalname, owner_type: attachment.owner_type, owner_id: attachment.owner_id, } }; } }