mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	fix(server): thumbnail content type not being passed to stream handle (#3137)
* asset mimetype instead of application/octet-stream * use thumbnail mimetype instead * narrowed openapi spec * thumbnail format validation * JPEG fallback, `getThumbnailPath` returns format * return content type in `getThumbnailPath` * moved `format` validation to dto * removed unused import * moved fallback warning * added `ApiOkResponse`
This commit is contained in:
		
							
								
								
									
										2
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							| @@ -822,7 +822,7 @@ Name | Type | Description  | Notes | |||||||
| ### HTTP request headers | ### HTTP request headers | ||||||
| 
 | 
 | ||||||
|  - **Content-Type**: Not defined |  - **Content-Type**: Not defined | ||||||
|  - **Accept**: application/octet-stream |  - **Accept**: image/jpeg, image/webp | ||||||
| 
 | 
 | ||||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/PersonApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/PersonApi.md
									
									
									
										generated
									
									
									
								
							| @@ -228,7 +228,7 @@ Name | Type | Description  | Notes | |||||||
| ### HTTP request headers | ### HTTP request headers | ||||||
| 
 | 
 | ||||||
|  - **Content-Type**: Not defined |  - **Content-Type**: Not defined | ||||||
|  - **Accept**: application/octet-stream |  - **Accept**: image/jpeg | ||||||
| 
 | 
 | ||||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -1673,7 +1673,13 @@ | |||||||
|         "responses": { |         "responses": { | ||||||
|           "200": { |           "200": { | ||||||
|             "content": { |             "content": { | ||||||
|               "application/octet-stream": { |               "image/jpeg": { | ||||||
|  |                 "schema": { | ||||||
|  |                   "type": "string", | ||||||
|  |                   "format": "binary" | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |               "image/webp": { | ||||||
|                 "schema": { |                 "schema": { | ||||||
|                   "type": "string", |                   "type": "string", | ||||||
|                   "format": "binary" |                   "format": "binary" | ||||||
| @@ -2704,7 +2710,7 @@ | |||||||
|         "responses": { |         "responses": { | ||||||
|           "200": { |           "200": { | ||||||
|             "content": { |             "content": { | ||||||
|               "application/octet-stream": { |               "image/jpeg": { | ||||||
|                 "schema": { |                 "schema": { | ||||||
|                   "type": "string", |                   "type": "string", | ||||||
|                   "format": "binary" |                   "format": "binary" | ||||||
|   | |||||||
| @@ -122,7 +122,11 @@ export class AssetController { | |||||||
|   @SharedLinkRoute() |   @SharedLinkRoute() | ||||||
|   @Get('/file/:id') |   @Get('/file/:id') | ||||||
|   @Header('Cache-Control', 'private, max-age=86400, no-transform') |   @Header('Cache-Control', 'private, max-age=86400, no-transform') | ||||||
|   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) |   @ApiOkResponse({ | ||||||
|  |     content: { | ||||||
|  |       'application/octet-stream': { schema: { type: 'string', format: 'binary' } }, | ||||||
|  |     }, | ||||||
|  |   }) | ||||||
|   serveFile( |   serveFile( | ||||||
|     @AuthUser() authUser: AuthUserDto, |     @AuthUser() authUser: AuthUserDto, | ||||||
|     @Headers() headers: Record<string, string>, |     @Headers() headers: Record<string, string>, | ||||||
| @@ -136,7 +140,12 @@ export class AssetController { | |||||||
|   @SharedLinkRoute() |   @SharedLinkRoute() | ||||||
|   @Get('/thumbnail/:id') |   @Get('/thumbnail/:id') | ||||||
|   @Header('Cache-Control', 'private, max-age=86400, no-transform') |   @Header('Cache-Control', 'private, max-age=86400, no-transform') | ||||||
|   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) |   @ApiOkResponse({ | ||||||
|  |     content: { | ||||||
|  |       'image/jpeg': { schema: { type: 'string', format: 'binary' } }, | ||||||
|  |       'image/webp': { schema: { type: 'string', format: 'binary' } }, | ||||||
|  |     }, | ||||||
|  |   }) | ||||||
|   getAssetThumbnail( |   getAssetThumbnail( | ||||||
|     @AuthUser() authUser: AuthUserDto, |     @AuthUser() authUser: AuthUserDto, | ||||||
|     @Headers() headers: Record<string, string>, |     @Headers() headers: Record<string, string>, | ||||||
|   | |||||||
| @@ -256,8 +256,8 @@ export class AssetService { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       const thumbnailPath = this.getThumbnailPath(asset, query.format); |       const [thumbnailPath, contentType] = this.getThumbnailPath(asset, query.format); | ||||||
|       return this.streamFile(thumbnailPath, res, headers); |       return this.streamFile(thumbnailPath, res, headers, contentType); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       res.header('Cache-Control', 'none'); |       res.header('Cache-Control', 'none'); | ||||||
|       this.logger.error(`Cannot create read stream for asset ${asset.id}`, 'getAssetThumbnail'); |       this.logger.error(`Cannot create read stream for asset ${asset.id}`, 'getAssetThumbnail'); | ||||||
| @@ -522,16 +522,17 @@ export class AssetService { | |||||||
|   private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { |   private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { | ||||||
|     switch (format) { |     switch (format) { | ||||||
|       case GetAssetThumbnailFormatEnum.WEBP: |       case GetAssetThumbnailFormatEnum.WEBP: | ||||||
|         if (asset.webpPath && asset.webpPath.length > 0) { |         if (asset.webpPath) { | ||||||
|           return asset.webpPath; |           return [asset.webpPath, 'image/webp']; | ||||||
|         } |         } | ||||||
|  |         this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`); | ||||||
|  |  | ||||||
|       case GetAssetThumbnailFormatEnum.JPEG: |       case GetAssetThumbnailFormatEnum.JPEG: | ||||||
|       default: |       default: | ||||||
|         if (!asset.resizePath) { |         if (!asset.resizePath) { | ||||||
|           throw new NotFoundException('resizePath not set'); |           throw new NotFoundException(`No thumbnail found for asset ${asset.id}`); | ||||||
|         } |         } | ||||||
|         return asset.resizePath; |         return [asset.resizePath, 'image/jpeg']; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { ApiProperty } from '@nestjs/swagger'; | import { ApiProperty } from '@nestjs/swagger'; | ||||||
| import { IsOptional } from 'class-validator'; | import { IsEnum, IsOptional } from 'class-validator'; | ||||||
|  |  | ||||||
| export enum GetAssetThumbnailFormatEnum { | export enum GetAssetThumbnailFormatEnum { | ||||||
|   JPEG = 'JPEG', |   JPEG = 'JPEG', | ||||||
| @@ -8,6 +8,7 @@ export enum GetAssetThumbnailFormatEnum { | |||||||
|  |  | ||||||
| export class GetAssetThumbnailDto { | export class GetAssetThumbnailDto { | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|  |   @IsEnum(GetAssetThumbnailFormatEnum) | ||||||
|   @ApiProperty({ |   @ApiProperty({ | ||||||
|     type: String, |     type: String, | ||||||
|     enum: GetAssetThumbnailFormatEnum, |     enum: GetAssetThumbnailFormatEnum, | ||||||
|   | |||||||
| @@ -43,7 +43,11 @@ export class PersonController { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Get(':id/thumbnail') |   @Get(':id/thumbnail') | ||||||
|   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) |   @ApiOkResponse({ | ||||||
|  |     content: { | ||||||
|  |       'image/jpeg': { schema: { type: 'string', format: 'binary' } }, | ||||||
|  |     }, | ||||||
|  |   }) | ||||||
|   getPersonThumbnail(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { |   getPersonThumbnail(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { | ||||||
|     return this.service.getThumbnail(authUser, id).then(asStreamableFile); |     return this.service.getThumbnail(authUser, id).then(asStreamableFile); | ||||||
|   } |   } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user