feat(attachments): minor fix and try catch for attachments controller

This commit is contained in:
Matthieu Haineault 2025-08-18 10:44:28 -04:00
parent e62b4cff1c
commit 30f7179fe6
2 changed files with 39 additions and 32 deletions

View File

@ -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,21 +25,30 @@ export class AttachmentsController {
@Get(':id')
async getById(@Param('id') id: string, @Res() res: Response) {
const num_id = Number(id);
if(!Number.isFinite(num_id)) throw new NotFoundException('Invalid id');
const att = await this.prisma.attachments.findUnique({
where: { id: Number(id) },
where: { id: num_id },
include: { blob: true },
});
if (!att) throw new NotFoundException();
const abs = path.join(resolveAttachmentsRoot(), att.blob.storage_path);
const stat = await fsp.stat(abs);
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');
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);
}
@ -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,

View File

@ -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 };
}
}