mirror of
https://github.com/immich-app/immich.git
synced 2025-06-16 03:54:44 -04:00
* feat(server): extract full-size previews from RAW images * feat(web): load fullsize preview for RAW images when zoomed in * refactor: tweaks for code review * refactor: rename "converted" preview/assets to "fullsize" * feat(web/server): fullsize preview for non-web-friendly images * feat: tweaks for code review * feat(server): require ASSET_DOWNLOAD premission for fullsize previews * test: fix types and interfaces * chore: gen open-api * feat(server): keep only essential exif in fullsize preview * chore: regen openapi * test: revert unnecessary timeout * feat: move full-size preview config to standalone entry * feat(i18n): update en texts * fix: don't return fullsizePath when disabled * test: full-size previews * test(web): full-size previews * chore: make open-api * feat(server): redirect to preview/original URL when fullsize thumbnail not available * fix(server): delete fullsize preview image on thumbnail regen after fullsize preview turned off * refactor(server): AssetRepository.deleteFiles with Kysely * fix(server): type of MediaRepository.writeExif * minor simplification * minor styling changes and condensed wording * simplify * chore: reuild open-api * test(server): fix media.service tests * test(web): fix photo-viewer test * fix(server): use fullsize image when requested * fix file path extension * formatting * use fullsize when zooming back out or when "display original photos" is enabled * simplify condition --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
204 lines
6.5 KiB
TypeScript
204 lines
6.5 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Next,
|
|
Param,
|
|
ParseFilePipe,
|
|
Post,
|
|
Put,
|
|
Query,
|
|
Req,
|
|
Res,
|
|
UploadedFiles,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { ApiBody, ApiConsumes, ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
import { NextFunction, Request, Response } from 'express';
|
|
import { EndpointLifecycle } from 'src/decorators';
|
|
import {
|
|
AssetBulkUploadCheckResponseDto,
|
|
AssetMediaResponseDto,
|
|
AssetMediaStatus,
|
|
CheckExistingAssetsResponseDto,
|
|
} from 'src/dtos/asset-media-response.dto';
|
|
import {
|
|
AssetBulkUploadCheckDto,
|
|
AssetMediaCreateDto,
|
|
AssetMediaOptionsDto,
|
|
AssetMediaReplaceDto,
|
|
AssetMediaSize,
|
|
CheckExistingAssetsDto,
|
|
UploadFieldName,
|
|
} from 'src/dtos/asset-media.dto';
|
|
import { AuthDto } from 'src/dtos/auth.dto';
|
|
import { ImmichHeader, RouteKey } from 'src/enum';
|
|
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
|
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
|
import { FileUploadInterceptor, getFiles } from 'src/middleware/file-upload.interceptor';
|
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
|
import { UploadFiles } from 'src/types';
|
|
import { ImmichFileResponse, sendFile } from 'src/utils/file';
|
|
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
|
|
|
@ApiTags('Assets')
|
|
@Controller(RouteKey.ASSET)
|
|
export class AssetMediaController {
|
|
constructor(
|
|
private logger: LoggingRepository,
|
|
private service: AssetMediaService,
|
|
) {}
|
|
|
|
@Post()
|
|
@UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor)
|
|
@ApiConsumes('multipart/form-data')
|
|
@ApiHeader({
|
|
name: ImmichHeader.CHECKSUM,
|
|
description: 'sha1 checksum that can be used for duplicate detection before the file is uploaded',
|
|
required: false,
|
|
})
|
|
@ApiBody({ description: 'Asset Upload Information', type: AssetMediaCreateDto })
|
|
@Authenticated({ sharedLink: true })
|
|
async uploadAsset(
|
|
@Auth() auth: AuthDto,
|
|
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles,
|
|
@Body() dto: AssetMediaCreateDto,
|
|
@Res({ passthrough: true }) res: Response,
|
|
): Promise<AssetMediaResponseDto> {
|
|
const { file, sidecarFile } = getFiles(files);
|
|
const responseDto = await this.service.uploadAsset(auth, dto, file, sidecarFile);
|
|
|
|
if (responseDto.status === AssetMediaStatus.DUPLICATE) {
|
|
res.status(HttpStatus.OK);
|
|
}
|
|
|
|
return responseDto;
|
|
}
|
|
|
|
@Get(':id/original')
|
|
@FileResponse()
|
|
@Authenticated({ sharedLink: true })
|
|
async downloadAsset(
|
|
@Auth() auth: AuthDto,
|
|
@Param() { id }: UUIDParamDto,
|
|
@Res() res: Response,
|
|
@Next() next: NextFunction,
|
|
) {
|
|
await sendFile(res, next, () => this.service.downloadOriginal(auth, id), this.logger);
|
|
}
|
|
|
|
/**
|
|
* Replace the asset with new file, without changing its id
|
|
*/
|
|
@Put(':id/original')
|
|
@UseInterceptors(FileUploadInterceptor)
|
|
@ApiConsumes('multipart/form-data')
|
|
@EndpointLifecycle({ addedAt: 'v1.106.0' })
|
|
@ApiOperation({
|
|
summary: 'replaceAsset',
|
|
description: 'Replace the asset with new file, without changing its id',
|
|
})
|
|
@Authenticated({ sharedLink: true })
|
|
async replaceAsset(
|
|
@Auth() auth: AuthDto,
|
|
@Param() { id }: UUIDParamDto,
|
|
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator([UploadFieldName.ASSET_DATA])] }))
|
|
files: UploadFiles,
|
|
@Body() dto: AssetMediaReplaceDto,
|
|
@Res({ passthrough: true }) res: Response,
|
|
): Promise<AssetMediaResponseDto> {
|
|
const { file } = getFiles(files);
|
|
const responseDto = await this.service.replaceAsset(auth, id, dto, file);
|
|
if (responseDto.status === AssetMediaStatus.DUPLICATE) {
|
|
res.status(HttpStatus.OK);
|
|
}
|
|
return responseDto;
|
|
}
|
|
|
|
@Get(':id/thumbnail')
|
|
@FileResponse()
|
|
@Authenticated({ sharedLink: true })
|
|
async viewAsset(
|
|
@Auth() auth: AuthDto,
|
|
@Param() { id }: UUIDParamDto,
|
|
@Query() dto: AssetMediaOptionsDto,
|
|
@Req() req: Request,
|
|
@Res() res: Response,
|
|
@Next() next: NextFunction,
|
|
) {
|
|
const viewThumbnailRes = await this.service.viewThumbnail(auth, id, dto);
|
|
|
|
if (viewThumbnailRes instanceof ImmichFileResponse) {
|
|
await sendFile(res, next, () => Promise.resolve(viewThumbnailRes), this.logger);
|
|
} else {
|
|
// viewThumbnailRes is a AssetMediaRedirectResponse
|
|
// which redirects to the original asset or a specific size to make better use of caching
|
|
const { targetSize } = viewThumbnailRes;
|
|
const [reqPath, reqSearch] = req.url.split('?');
|
|
let redirPath: string;
|
|
const redirSearchParams = new URLSearchParams(reqSearch);
|
|
if (targetSize === 'original') {
|
|
// relative path to this.downloadAsset
|
|
redirPath = 'original';
|
|
redirSearchParams.delete('size');
|
|
} else if (Object.values(AssetMediaSize).includes(targetSize)) {
|
|
redirPath = reqPath;
|
|
redirSearchParams.set('size', targetSize);
|
|
} else {
|
|
throw new Error('Invalid targetSize: ' + targetSize);
|
|
}
|
|
const finalRedirPath = redirPath + '?' + redirSearchParams.toString();
|
|
return res.redirect(finalRedirPath);
|
|
}
|
|
}
|
|
|
|
@Get(':id/video/playback')
|
|
@FileResponse()
|
|
@Authenticated({ sharedLink: true })
|
|
async playAssetVideo(
|
|
@Auth() auth: AuthDto,
|
|
@Param() { id }: UUIDParamDto,
|
|
@Res() res: Response,
|
|
@Next() next: NextFunction,
|
|
) {
|
|
await sendFile(res, next, () => this.service.playbackVideo(auth, id), this.logger);
|
|
}
|
|
|
|
/**
|
|
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
|
*/
|
|
@Post('exist')
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOperation({
|
|
summary: 'checkExistingAssets',
|
|
description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup',
|
|
})
|
|
@Authenticated()
|
|
checkExistingAssets(
|
|
@Auth() auth: AuthDto,
|
|
@Body() dto: CheckExistingAssetsDto,
|
|
): Promise<CheckExistingAssetsResponseDto> {
|
|
return this.service.checkExistingAssets(auth, dto);
|
|
}
|
|
|
|
/**
|
|
* Checks if assets exist by checksums
|
|
*/
|
|
@Post('bulk-upload-check')
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOperation({
|
|
summary: 'checkBulkUpload',
|
|
description: 'Checks if assets exist by checksums',
|
|
})
|
|
@Authenticated()
|
|
checkBulkUpload(
|
|
@Auth() auth: AuthDto,
|
|
@Body() dto: AssetBulkUploadCheckDto,
|
|
): Promise<AssetBulkUploadCheckResponseDto> {
|
|
return this.service.bulkUploadCheck(auth, dto);
|
|
}
|
|
}
|