mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	fix(server): use thumbnail content type instead of application/octet-stream (#3075)
* 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
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 | ||||
| 
 | ||||
|  - **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) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										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 | ||||
| 
 | ||||
|  - **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) | ||||
| 
 | ||||
|   | ||||
| @@ -1673,7 +1673,13 @@ | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "content": { | ||||
|               "application/octet-stream": { | ||||
|               "image/jpeg": { | ||||
|                 "schema": { | ||||
|                   "type": "string", | ||||
|                   "format": "binary" | ||||
|                 } | ||||
|               }, | ||||
|               "image/webp": { | ||||
|                 "schema": { | ||||
|                   "type": "string", | ||||
|                   "format": "binary" | ||||
| @@ -2704,7 +2710,7 @@ | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "content": { | ||||
|               "application/octet-stream": { | ||||
|               "image/jpeg": { | ||||
|                 "schema": { | ||||
|                   "type": "string", | ||||
|                   "format": "binary" | ||||
|   | ||||
| @@ -19,7 +19,7 @@ import { | ||||
|   ValidationPipe, | ||||
| } from '@nestjs/common'; | ||||
| import { FileFieldsInterceptor } from '@nestjs/platform-express'; | ||||
| import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger'; | ||||
| import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; | ||||
| import { Response as Res } from 'express'; | ||||
| import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard'; | ||||
| import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; | ||||
| @@ -122,7 +122,6 @@ export class AssetController { | ||||
|   @SharedLinkRoute() | ||||
|   @Get('/file/:id') | ||||
|   @Header('Cache-Control', 'private, max-age=86400, no-transform') | ||||
|   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) | ||||
|   serveFile( | ||||
|     @AuthUser() authUser: AuthUserDto, | ||||
|     @Headers() headers: Record<string, string>, | ||||
| @@ -136,7 +135,6 @@ export class AssetController { | ||||
|   @SharedLinkRoute() | ||||
|   @Get('/thumbnail/:id') | ||||
|   @Header('Cache-Control', 'private, max-age=86400, no-transform') | ||||
|   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) | ||||
|   getAssetThumbnail( | ||||
|     @AuthUser() authUser: AuthUserDto, | ||||
|     @Headers() headers: Record<string, string>, | ||||
|   | ||||
| @@ -256,8 +256,8 @@ export class AssetService { | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const thumbnailPath = this.getThumbnailPath(asset, query.format); | ||||
|       return this.streamFile(thumbnailPath, res, headers); | ||||
|       const [thumbnailPath, contentType] = this.getThumbnailPath(asset, query.format); | ||||
|       return this.streamFile(thumbnailPath, res, headers, contentType); | ||||
|     } catch (e) { | ||||
|       res.header('Cache-Control', 'none'); | ||||
|       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) { | ||||
|     switch (format) { | ||||
|       case GetAssetThumbnailFormatEnum.WEBP: | ||||
|         if (asset.webpPath && asset.webpPath.length > 0) { | ||||
|           return asset.webpPath; | ||||
|         if (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: | ||||
|       default: | ||||
|         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 { IsOptional } from 'class-validator'; | ||||
| import { IsEnum, IsOptional } from 'class-validator'; | ||||
|  | ||||
| export enum GetAssetThumbnailFormatEnum { | ||||
|   JPEG = 'JPEG', | ||||
| @@ -8,6 +8,7 @@ export enum GetAssetThumbnailFormatEnum { | ||||
|  | ||||
| export class GetAssetThumbnailDto { | ||||
|   @IsOptional() | ||||
|   @IsEnum(GetAssetThumbnailFormatEnum) | ||||
|   @ApiProperty({ | ||||
|     type: String, | ||||
|     enum: GetAssetThumbnailFormatEnum, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import { | ||||
|   PersonUpdateDto, | ||||
| } from '@app/domain'; | ||||
| import { Body, Controller, Get, Param, Put, StreamableFile } from '@nestjs/common'; | ||||
| import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; | ||||
| import { ApiTags } from '@nestjs/swagger'; | ||||
| import { Authenticated, AuthUser } from '../app.guard'; | ||||
| import { UseValidation } from '../app.utils'; | ||||
| import { UUIDParamDto } from './dto/uuid-param.dto'; | ||||
| @@ -43,7 +43,6 @@ export class PersonController { | ||||
|   } | ||||
|  | ||||
|   @Get(':id/thumbnail') | ||||
|   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) | ||||
|   getPersonThumbnail(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { | ||||
|     return this.service.getThumbnail(authUser, id).then(asStreamableFile); | ||||
|   } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user