mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:49:11 -04: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:
		
							parent
							
								
									6f4449d5e9
								
							
						
					
					
						commit
						3cc77d945b
					
				
							
								
								
									
										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" | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ import { | |||||||
|   ValidationPipe, |   ValidationPipe, | ||||||
| } from '@nestjs/common'; | } from '@nestjs/common'; | ||||||
| import { FileFieldsInterceptor } from '@nestjs/platform-express'; | 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 { Response as Res } from 'express'; | ||||||
| import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard'; | import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard'; | ||||||
| import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; | import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; | ||||||
| @ -122,7 +122,6 @@ 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' } } } }) |  | ||||||
|   serveFile( |   serveFile( | ||||||
|     @AuthUser() authUser: AuthUserDto, |     @AuthUser() authUser: AuthUserDto, | ||||||
|     @Headers() headers: Record<string, string>, |     @Headers() headers: Record<string, string>, | ||||||
| @ -136,7 +135,6 @@ 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' } } } }) |  | ||||||
|   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, | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ import { | |||||||
|   PersonUpdateDto, |   PersonUpdateDto, | ||||||
| } from '@app/domain'; | } from '@app/domain'; | ||||||
| import { Body, Controller, Get, Param, Put, StreamableFile } from '@nestjs/common'; | 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 { Authenticated, AuthUser } from '../app.guard'; | ||||||
| import { UseValidation } from '../app.utils'; | import { UseValidation } from '../app.utils'; | ||||||
| import { UUIDParamDto } from './dto/uuid-param.dto'; | import { UUIDParamDto } from './dto/uuid-param.dto'; | ||||||
| @ -43,7 +43,6 @@ export class PersonController { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @Get(':id/thumbnail') |   @Get(':id/thumbnail') | ||||||
|   @ApiOkResponse({ content: { 'application/octet-stream': { 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); | ||||||
|   } |   } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user