From 30f7179fe665cfe7e9e2760f3d544e86809a7afb Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Mon, 18 Aug 2025 10:44:28 -0400 Subject: [PATCH] feat(attachments): minor fix and try catch for attachments controller --- .../controllers/attachments.controller.ts | 67 ++++++++++--------- .../services/disk-storage.service.ts | 4 +- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/modules/attachments/controllers/attachments.controller.ts b/src/modules/attachments/controllers/attachments.controller.ts index 71c7eec..1defed3 100644 --- a/src/modules/attachments/controllers/attachments.controller.ts +++ b/src/modules/attachments/controllers/attachments.controller.ts @@ -2,17 +2,15 @@ import { FileInterceptor } from "@nestjs/platform-express"; import { DiskStorageService } from "../services/disk-storage.service"; import { Controller,NotFoundException, UseInterceptors, Post, Get, Param, Res, - UploadedFile, BadRequestException, UnsupportedMediaTypeException, Body, - Delete, NotImplementedException + UploadedFile, BadRequestException, UnsupportedMediaTypeException, Body, Delete } from "@nestjs/common"; import { maxUploadBytes, allowedMimes } from "../config/upload.config"; import { memoryStorage } from 'multer'; -import { fileTypeFromBuffer, fileTypeFromFile } from "file-type"; +import { fileTypeFromBuffer } from "file-type"; import { Readable } from "node:stream"; import { PrismaService } from "src/prisma/prisma.service"; import { UploadMetaAttachmentsDto } from "../dtos/upload-meta-attachments.dto"; import { resolveAttachmentsRoot } from "src/config/attachment.config"; -import { casPathFor } from "../utils/cas.util"; import * as path from 'node:path'; import { promises as fsp } from 'node:fs'; import { createReadStream } from "node:fs"; @@ -27,23 +25,32 @@ export class AttachmentsController { @Get(':id') async getById(@Param('id') id: string, @Res() res: Response) { - const att = await this.prisma.attachments.findUnique({ - where: { id: Number(id) }, - include: { blob: true }, - }); - if (!att) throw new NotFoundException(); + const num_id = Number(id); + if(!Number.isFinite(num_id)) throw new NotFoundException('Invalid id'); - const abs = path.join(resolveAttachmentsRoot(), att.blob.storage_path); - const stat = await fsp.stat(abs); + const att = await this.prisma.attachments.findUnique({ + where: { id: num_id }, + include: { blob: true }, + }); + if (!att) throw new NotFoundException(); - res.setHeader('Content-Type', att.blob.mime); - res.setHeader('Content-Length', String(stat.size)); - res.setHeader('ETag', `"sha256-${att.blob.sha256}"`); - res.setHeader('Last-Modified', stat.mtime.toUTCString()); - res.setHeader('Cache-Control', 'private, max-age=31536000, immutable'); - res.setHeader('X-Content-Type-Options', 'nosniff'); - createReadStream(abs).pipe(res); + const abs = path.join(resolveAttachmentsRoot(), att.blob.storage_path); + let stat; + try { + stat = await fsp.stat(abs); + }catch { + throw new NotFoundException('File not found'); + } + + res.set('Content-Type', att.blob.mime); + res.set('Content-Length', String(stat.size)); + res.set('ETag', `"sha256-${att.blob.sha256}"`); + res.set('Last-Modified', stat.mtime.toUTCString()); + res.set('Cache-Control', 'private, max-age=31536000, immutable'); + res.set('X-Content-Type-Options', 'nosniff'); + + createReadStream(abs).pipe(res); } // DEV version, uncomment once connected to DB and distant server @@ -60,10 +67,10 @@ export class AttachmentsController { }); // decrement refcount - const dec = await tx.$executeRawUnsafe( + const dec = await tx.$executeRaw `UPDATE "Blobs" SET refcount = refcount - 1 - WHERE sha256 = $1 AND refcount > 0`, att.sha256, - ); + WHERE sha256 = ${att.sha256} AND refcount > 0` + ; return { ok: true, decremented: dec > 0 }; }); @@ -83,16 +90,16 @@ export class AttachmentsController { //magic detection using binary signature const kind = await fileTypeFromBuffer(file.buffer).catch(() => null); - const detectedMime = kind?.mime || file.mimetype || 'application/octet-stream'; + const detected_mime = kind?.mime || file.mimetype || 'application/octet-stream'; //strict whitelist - if(!allowedMimes().includes(detectedMime)) { - throw new UnsupportedMediaTypeException(`This type is not supported: ${detectedMime}`); + if(!allowedMimes().includes(detected_mime)) { + throw new UnsupportedMediaTypeException(`This type is not supported: ${detected_mime}`); } //Saving FS (hash + CAS + unDupes) const stream = Readable.from(file.buffer); - const { sha256, storagePath, size } = await this.disk.saveStreamAndHash(stream); + const { sha256, storage_path, size } = await this.disk.saveStreamAndHash(stream); const now = new Date(); const attachment = await this.prisma.$transaction(async (tx) => { @@ -101,15 +108,15 @@ export class AttachmentsController { where: { sha256 }, create: { sha256, - storage_path: storagePath, + storage_path: storage_path, size, - mime: detectedMime, + mime: detected_mime, refcount: 1, created_at: now, }, update: { //only increment, does not change the storage path refcount: { increment: 1 }, - mime: detectedMime, //update mime and size to keep last image + mime: detected_mime, //update mime and size to keep last image size, }, }); @@ -133,9 +140,9 @@ export class AttachmentsController { ok: true, id: attachment.id, sha256, - storagePath: storagePath, + storage_path: storage_path, size, - mime: detectedMime, + mime: detected_mime, original_name: file.originalname, owner_type: attachment.owner_type, owner_id: attachment.owner_id, diff --git a/src/modules/attachments/services/disk-storage.service.ts b/src/modules/attachments/services/disk-storage.service.ts index 3427926..0ef8e22 100644 --- a/src/modules/attachments/services/disk-storage.service.ts +++ b/src/modules/attachments/services/disk-storage.service.ts @@ -5,7 +5,7 @@ import { join, dirname } from 'node:path'; import { pipeline } from 'node:stream/promises'; import { ATT_TMP_DIR, resolveAttachmentsRoot } from 'src/config/attachment.config'; -export type SaveResult = { sha256:string, storagePath:string, size:number}; +export type SaveResult = { sha256:string, storage_path:string, size:number}; export class DiskStorageService { private root = resolveAttachmentsRoot(); @@ -64,6 +64,6 @@ export class DiskStorageService { } const size = statSync(finalAbs).size; - return { sha256: sha, storagePath: rel, size }; + return { sha256: sha, storage_path: rel, size }; } } \ No newline at end of file