2024-05-23 20:26:22 -04:00
import {
Body ,
Controller ,
2024-05-31 13:44:04 -04:00
Get ,
2024-05-24 21:02:22 -04:00
HttpCode ,
2024-05-23 20:26:22 -04:00
HttpStatus ,
2024-05-31 13:44:04 -04:00
Next ,
2024-05-23 20:26:22 -04:00
Param ,
ParseFilePipe ,
2024-05-24 21:02:22 -04:00
Post ,
2024-05-31 13:44:04 -04:00
Query ,
2025-04-01 01:24:28 +08:00
Req ,
2024-05-23 20:26:22 -04:00
Res ,
UploadedFiles ,
UseInterceptors ,
} from '@nestjs/common' ;
2026-01-09 05:52:31 +01:00
import { ApiBody , ApiConsumes , ApiHeader , ApiResponse , ApiTags } from '@nestjs/swagger' ;
2025-04-01 01:24:28 +08:00
import { NextFunction , Request , Response } from 'express' ;
2025-11-13 08:18:43 -05:00
import { Endpoint , HistoryBuilder } from 'src/decorators' ;
2024-05-24 21:02:22 -04:00
import {
AssetBulkUploadCheckResponseDto ,
AssetMediaResponseDto ,
2024-05-31 13:44:04 -04:00
AssetMediaStatus ,
2024-05-24 21:02:22 -04:00
} from 'src/dtos/asset-media-response.dto' ;
import {
AssetBulkUploadCheckDto ,
2024-05-31 13:44:04 -04:00
AssetMediaCreateDto ,
AssetMediaOptionsDto ,
2025-04-01 01:24:28 +08:00
AssetMediaSize ,
2024-05-24 21:02:22 -04:00
} from 'src/dtos/asset-media.dto' ;
2026-01-09 17:59:52 -05:00
import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto' ;
2024-10-17 13:17:32 -04:00
import { AuthDto } from 'src/dtos/auth.dto' ;
2025-11-11 17:01:14 -05:00
import { ApiTag , ImmichHeader , Permission , RouteKey } from 'src/enum' ;
2024-05-31 13:44:04 -04:00
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor' ;
import { Auth , Authenticated , FileResponse } from 'src/middleware/auth.guard' ;
2025-02-11 17:15:56 -05:00
import { FileUploadInterceptor , getFiles } from 'src/middleware/file-upload.interceptor' ;
2025-01-23 08:31:30 -05:00
import { LoggingRepository } from 'src/repositories/logging.repository' ;
2024-05-23 20:26:22 -04:00
import { AssetMediaService } from 'src/services/asset-media.service' ;
2025-02-11 17:15:56 -05:00
import { UploadFiles } from 'src/types' ;
2025-04-01 01:24:28 +08:00
import { ImmichFileResponse , sendFile } from 'src/utils/file' ;
2024-05-23 20:26:22 -04:00
import { FileNotEmptyValidator , UUIDParamDto } from 'src/validation' ;
2025-11-11 17:01:14 -05:00
@ApiTags ( ApiTag . Assets )
2025-07-15 14:50:13 -04:00
@Controller ( RouteKey . Asset )
2024-05-23 20:26:22 -04:00
export class AssetMediaController {
constructor (
2025-01-23 08:31:30 -05:00
private logger : LoggingRepository ,
2024-05-23 20:26:22 -04:00
private service : AssetMediaService ,
) { }
2024-05-31 13:44:04 -04:00
@Post ( )
2025-11-11 17:01:14 -05:00
@Authenticated ( { permission : Permission.AssetUpload , sharedLink : true } )
2024-05-31 13:44:04 -04:00
@UseInterceptors ( AssetUploadInterceptor , FileUploadInterceptor )
@ApiConsumes ( 'multipart/form-data' )
@ApiHeader ( {
2025-07-15 14:50:13 -04:00
name : ImmichHeader.Checksum ,
2024-05-31 13:44:04 -04:00
description : 'sha1 checksum that can be used for duplicate detection before the file is uploaded' ,
required : false ,
} )
@ApiBody ( { description : 'Asset Upload Information' , type : AssetMediaCreateDto } )
2026-01-09 05:52:31 +01:00
@ApiResponse ( {
status : 200 ,
description : 'Asset is a duplicate' ,
type : AssetMediaResponseDto ,
} )
@ApiResponse ( {
status : 201 ,
description : 'Asset uploaded successfully' ,
type : AssetMediaResponseDto ,
} )
2025-11-13 08:18:43 -05:00
@Endpoint ( {
2025-11-11 17:01:14 -05:00
summary : 'Upload asset' ,
description : 'Uploads a new asset to the server.' ,
2025-11-13 08:18:43 -05:00
history : new HistoryBuilder ( ) . added ( 'v1' ) . beta ( 'v1' ) . stable ( 'v2' ) ,
2025-11-11 17:01:14 -05:00
} )
2024-05-31 13:44:04 -04:00
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 ( )
2025-07-25 15:25:23 -04:00
@Authenticated ( { permission : Permission.AssetDownload , sharedLink : true } )
2025-11-13 08:18:43 -05:00
@Endpoint ( {
2025-11-11 17:01:14 -05:00
summary : 'Download original asset' ,
description : 'Downloads the original file of the specified asset.' ,
2025-11-13 08:18:43 -05:00
history : new HistoryBuilder ( ) . added ( 'v1' ) . beta ( 'v1' ) . stable ( 'v2' ) ,
2025-11-11 17:01:14 -05:00
} )
2024-05-31 13:44:04 -04:00
async downloadAsset (
@Auth ( ) auth : AuthDto ,
@Param ( ) { id } : UUIDParamDto ,
2026-01-09 17:59:52 -05:00
@Query ( ) dto : AssetDownloadOriginalDto ,
2024-05-31 13:44:04 -04:00
@Res ( ) res : Response ,
@Next ( ) next : NextFunction ,
) {
2026-01-09 17:59:52 -05:00
await sendFile ( res , next , ( ) = > this . service . downloadOriginal ( auth , id , dto ) , this . logger ) ;
2024-05-31 13:44:04 -04:00
}
@Get ( ':id/thumbnail' )
@FileResponse ( )
2025-07-25 15:25:23 -04:00
@Authenticated ( { permission : Permission.AssetView , sharedLink : true } )
2025-11-13 08:18:43 -05:00
@Endpoint ( {
2025-11-11 17:01:14 -05:00
summary : 'View asset thumbnail' ,
2026-01-23 15:42:21 +01:00
description :
'Retrieve the thumbnail image for the specified asset. Viewing the fullsize thumbnail might redirect to downloadAsset, which requires a different permission.' ,
2025-11-13 08:18:43 -05:00
history : new HistoryBuilder ( ) . added ( 'v1' ) . beta ( 'v1' ) . stable ( 'v2' ) ,
2025-11-11 17:01:14 -05:00
} )
2024-05-31 13:44:04 -04:00
async viewAsset (
@Auth ( ) auth : AuthDto ,
@Param ( ) { id } : UUIDParamDto ,
@Query ( ) dto : AssetMediaOptionsDto ,
2025-04-01 01:24:28 +08:00
@Req ( ) req : Request ,
2024-05-31 13:44:04 -04:00
@Res ( ) res : Response ,
@Next ( ) next : NextFunction ,
) {
2026-04-15 15:43:09 +02:00
if ( dto . size === AssetMediaSize . Original ) {
this . logger . deprecate (
'Calling the thumbnail endpoint with size=original is deprecated. Use the :id/original endpoint instead' ,
) ;
const [ _ , reqSearch ] = req . url . split ( '?' ) ;
const redirSearchParams = new URLSearchParams ( reqSearch ) ;
redirSearchParams . delete ( 'size' ) ;
return res . redirect ( 'original' + '?' + redirSearchParams . toString ( ) ) ;
}
2025-04-01 01:24:28 +08:00
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 ) ;
}
2024-05-31 13:44:04 -04:00
}
@Get ( ':id/video/playback' )
@FileResponse ( )
2025-07-25 15:25:23 -04:00
@Authenticated ( { permission : Permission.AssetView , sharedLink : true } )
2025-11-13 08:18:43 -05:00
@Endpoint ( {
2025-11-11 17:01:14 -05:00
summary : 'Play asset video' ,
description : 'Streams the video file for the specified asset. This endpoint also supports byte range requests.' ,
2025-11-13 08:18:43 -05:00
history : new HistoryBuilder ( ) . added ( 'v1' ) . beta ( 'v1' ) . stable ( 'v2' ) ,
2025-11-11 17:01:14 -05:00
} )
2024-05-31 13:44:04 -04:00
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 ) ;
}
2024-05-24 21:02:22 -04:00
@Post ( 'bulk-upload-check' )
2025-09-02 21:21:14 +02:00
@Authenticated ( { permission : Permission.AssetUpload } )
2025-11-13 08:18:43 -05:00
@Endpoint ( {
2025-11-11 17:01:14 -05:00
summary : 'Check bulk upload' ,
description : 'Determine which assets have already been uploaded to the server based on their SHA1 checksums.' ,
2025-11-13 08:18:43 -05:00
history : new HistoryBuilder ( ) . added ( 'v1' ) . beta ( 'v1' ) . stable ( 'v2' ) ,
2025-01-29 08:58:10 -08:00
} )
2025-08-08 15:56:37 -04:00
@HttpCode ( HttpStatus . OK )
2024-05-24 21:02:22 -04:00
checkBulkUpload (
@Auth ( ) auth : AuthDto ,
@Body ( ) dto : AssetBulkUploadCheckDto ,
) : Promise < AssetBulkUploadCheckResponseDto > {
return this . service . bulkUploadCheck ( auth , dto ) ;
}
2024-05-23 20:26:22 -04:00
}