diff --git a/src/modules/asset/application/blog-live2d-asset.service.ts b/src/modules/asset/application/blog-live2d-asset.service.ts index e1ee2a1..9e14c36 100644 --- a/src/modules/asset/application/blog-live2d-asset.service.ts +++ b/src/modules/asset/application/blog-live2d-asset.service.ts @@ -1,4 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { MinioClientService } from './asset-minio.service'; import type { @@ -11,6 +15,24 @@ const DEFAULT_LIVE2D_BUCKET = 'kt-template-online'; const DEFAULT_LIVE2D_PREFIX = 'blog/live2d/pio'; const MAX_DECODE_DEPTH = 6; +/** + * Detects MinIO/S3 object-missing errors from `statObject` and `getObject`. + * @param error - Unknown failure thrown by the MinIO client while resolving a runtime object. + * @returns `true` when the failure means the requested object key does not exist. + */ +function isMinioObjectNotFound(error: unknown): boolean { + if (!error || typeof error !== 'object') { + return false; + } + + const candidate = error as { code?: unknown; message?: unknown }; + return ( + candidate.code === 'NotFound' || + candidate.code === 'NoSuchKey' || + candidate.message === 'Not Found' + ); +} + @Injectable() export class BlogLive2DAssetService { /** @@ -54,10 +76,17 @@ export class BlogLive2DAssetService { version: string, objectPath: BlogLive2DRuntimeAssetPath, ): Promise { - return this.minioClientService.getObject( - this.resolveRuntimeObjectPath(version, objectPath), - this.getBucketName(), - ); + try { + return await this.minioClientService.getObject( + this.resolveRuntimeObjectPath(version, objectPath), + this.getBucketName(), + ); + } catch (error) { + if (isMinioObjectNotFound(error)) { + throw new NotFoundException('Live2D runtime asset not found'); + } + throw error; + } } /** diff --git a/test/asset/blog-live2d-asset.service.spec.ts b/test/asset/blog-live2d-asset.service.spec.ts index 014afb2..e3e834a 100644 --- a/test/asset/blog-live2d-asset.service.spec.ts +++ b/test/asset/blog-live2d-asset.service.spec.ts @@ -117,6 +117,28 @@ describe('BlogLive2DAssetService', () => { ); }); + it('maps missing MinIO runtime objects to HTTP 404', async () => { + const minio = createMinio(); + minio.getObject.mockRejectedValueOnce( + Object.assign(new Error('Not Found'), { code: 'NotFound' }), + ); + const service = new BlogLive2DAssetService( + minio as never, + createConfig() as never, + ); + + await expect( + service.getRuntimeObject('v1', ['manifest.json']), + ).rejects.toMatchObject({ + status: HttpStatus.NOT_FOUND, + message: 'Live2D runtime asset not found', + }); + expect(minio.getObject).toHaveBeenCalledWith( + 'blog/live2d/pio/v1/manifest.json', + 'kt-template-online', + ); + }); + it('rejects runtime path traversal before touching MinIO', async () => { const minio = createMinio(); const service = new BlogLive2DAssetService(