From 2a300496000d0d42b39af16f518cc34c02748574 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sat, 4 Jul 2026 10:02:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3Blog=20Live2D=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=E8=B5=84=E6=BA=90=E7=8A=B6=E6=80=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/blog-live2d-asset.service.ts | 39 ++++++++++++++++--- test/asset/blog-live2d-asset.service.spec.ts | 22 +++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) 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(