From a5cc40846938e713cf3c7147c58738fc722f7f55 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sat, 8 Jul 2023 16:07:56 -0400 Subject: [PATCH] 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` --- mobile/openapi/doc/AssetApi.md | Bin 56109 -> 56107 bytes mobile/openapi/doc/PersonApi.md | Bin 10296 -> 10282 bytes server/immich-openapi-specs.json | 10 ++++++++-- .../immich/api-v1/asset/asset.controller.ts | 13 +++++++++++-- .../src/immich/api-v1/asset/asset.service.ts | 13 +++++++------ .../asset/dto/get-asset-thumbnail.dto.ts | 3 ++- .../immich/controllers/person.controller.ts | 6 +++++- 7 files changed, 33 insertions(+), 12 deletions(-) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 319deb2acfff3bd01f7dbd55b5aedbc24d53a552..c71b4151d3adf98aef58c2fbed5757cba6e74eaf 100644 GIT binary patch delta 41 tcmZ3xjd}Gp<_)sdVwt&#>8biz1*z#e3J`XAYEr@Ehnx3o{#b1j4**`g5y}7n delta 26 icmZ3zjd|@h<_)sdlOJUBOct;@J-MYpU^7#VW;_6&s|y7H diff --git a/mobile/openapi/doc/PersonApi.md b/mobile/openapi/doc/PersonApi.md index dd1c0eb8e41bfe56159ddd2de18e166253d5cd69..aa37a294e1ad91c12d10211cd658a7d30b534080 100644 GIT binary patch delta 27 icmdlHuqt4Ki9AepW$h`s4=n9h+GcOgI3R6$$MC delta 29 kcmZ1#up?lDiTvbV4j!KT)WqD)cJgmH0I7TnA^-pY diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index caae449e25..5f664730f0 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -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" diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index c2d3e6b39d..99f6d02ab4 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -122,7 +122,11 @@ 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' } } } }) + @ApiOkResponse({ + content: { + 'application/octet-stream': { schema: { type: 'string', format: 'binary' } }, + }, + }) serveFile( @AuthUser() authUser: AuthUserDto, @Headers() headers: Record, @@ -136,7 +140,12 @@ 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' } } } }) + @ApiOkResponse({ + content: { + 'image/jpeg': { schema: { type: 'string', format: 'binary' } }, + 'image/webp': { schema: { type: 'string', format: 'binary' } }, + }, + }) getAssetThumbnail( @AuthUser() authUser: AuthUserDto, @Headers() headers: Record, diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 0a1ee8e0e2..26c0ca7bbe 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -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']; } } diff --git a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts b/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts index 5a8dc06872..ad0e755d6a 100644 --- a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts +++ b/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts @@ -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, diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/immich/controllers/person.controller.ts index 5752304598..6eb58844f4 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/immich/controllers/person.controller.ts @@ -43,7 +43,11 @@ export class PersonController { } @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) { return this.service.getThumbnail(authUser, id).then(asStreamableFile); }