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