mirror of
https://github.com/immich-app/immich.git
synced 2025-01-19 16:38:01 +02: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:
parent
d590dec159
commit
a5cc408469
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);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user