targo-backend/src/modules/attachments/services/attachment-upload.service.ts

87 lines
3.3 KiB
TypeScript

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<Result<any, string>> {
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,
}
};
}
}