87 lines
3.3 KiB
TypeScript
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,
|
|
}
|
|
};
|
|
}
|
|
} |