2025-09-29 18:09:06 -04:00
import { BadRequestException , Controller , Delete , Head , Options , Param , Patch , Post , Req , Res } from '@nestjs/common' ;
import { ApiHeader , ApiTags } from '@nestjs/swagger' ;
import { plainToInstance } from 'class-transformer' ;
import { validateSync } from 'class-validator' ;
import { Response } from 'express' ;
import { IncomingHttpHeaders } from 'node:http' ;
2025-09-24 13:56:46 -04:00
import { AuthDto } from 'src/dtos/auth.dto' ;
2025-09-29 18:09:06 -04:00
import { GetUploadStatusDto , ResumeUploadDto , StartUploadDto , UploadHeader } from 'src/dtos/upload.dto' ;
import { ImmichHeader , Permission } from 'src/enum' ;
import { Auth , Authenticated , AuthenticatedRequest } from 'src/middleware/auth.guard' ;
2025-09-24 13:56:46 -04:00
import { AssetUploadService } from 'src/services/asset-upload.service' ;
import { UUIDParamDto } from 'src/validation' ;
2025-09-29 18:09:06 -04:00
const apiInteropVersion = {
name : UploadHeader.InteropVersion ,
description : ` Indicates the version of the RUFH protocol supported by the client. ` ,
required : true ,
} ;
const apiUploadComplete = {
name : UploadHeader.UploadComplete ,
description :
'Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.' ,
required : true ,
} ;
const apiContentLength = {
name : UploadHeader.ContentLength ,
description : 'Non-negative size of the request body in bytes.' ,
required : true ,
} ;
2025-09-24 13:56:46 -04:00
@ApiTags ( 'Upload' )
@Controller ( 'upload' )
export class AssetUploadController {
constructor ( private service : AssetUploadService ) { }
2025-09-28 18:37:16 -04:00
@Post ( )
2025-09-24 13:56:46 -04:00
@Authenticated ( { sharedLink : true , permission : Permission.AssetUpload } )
2025-09-29 18:09:06 -04:00
@ApiHeader ( {
name : ImmichHeader.AssetData ,
description :
'Base64-encoded JSON of asset metadata. The expected content is the same as AssetMediaCreateDto, except that `filename` is required and `sidecarData` is ignored.' ,
required : true ,
} )
@ApiHeader ( {
name : UploadHeader.ReprDigest ,
description :
'Structured dictionary containing an SHA-1 checksum used to detect duplicate files and validate data integrity.' ,
required : true ,
} )
@ApiHeader ( apiInteropVersion )
@ApiHeader ( apiUploadComplete )
@ApiHeader ( apiContentLength )
startUpload ( @Req ( ) req : AuthenticatedRequest , @Res ( ) res : Response ) : Promise < void > {
const dto = this . getDto ( StartUploadDto , req . headers ) ;
console . log ( 'Starting upload with dto:' , JSON . stringify ( dto ) ) ;
return this . service . startUpload ( req , res , dto ) ;
2025-09-24 13:56:46 -04:00
}
2025-09-28 18:37:16 -04:00
@Patch ( ':id' )
2025-09-24 13:56:46 -04:00
@Authenticated ( { sharedLink : true , permission : Permission.AssetUpload } )
2025-09-29 18:09:06 -04:00
@ApiHeader ( {
name : UploadHeader.UploadOffset ,
description :
'Non-negative byte offset indicating the starting position of the data in the request body within the entire file.' ,
required : true ,
} )
@ApiHeader ( apiInteropVersion )
@ApiHeader ( apiUploadComplete )
@ApiHeader ( apiContentLength )
resumeUpload ( @Req ( ) req : AuthenticatedRequest , @Res ( ) res : Response , @Param ( ) { id } : UUIDParamDto ) {
const dto = this . getDto ( ResumeUploadDto , req . headers ) ;
console . log ( 'Resuming upload with dto:' , JSON . stringify ( dto ) ) ;
return this . service . resumeUpload ( req , res , id , dto ) ;
2025-09-24 13:56:46 -04:00
}
2025-09-28 18:37:16 -04:00
@Delete ( ':id' )
@Authenticated ( { sharedLink : true , permission : Permission.AssetUpload } )
2025-09-29 18:09:06 -04:00
cancelUpload ( @Auth ( ) auth : AuthDto , @Res ( ) res : Response , @Param ( ) { id } : UUIDParamDto ) {
return this . service . cancelUpload ( auth , id , res ) ;
2025-09-28 18:37:16 -04:00
}
2025-09-29 03:40:24 -04:00
@Head ( ':id' )
2025-09-28 18:37:16 -04:00
@Authenticated ( { sharedLink : true , permission : Permission.AssetUpload } )
2025-09-29 18:09:06 -04:00
@ApiHeader ( apiInteropVersion )
getUploadStatus ( @Req ( ) req : AuthenticatedRequest , @Res ( ) res : Response , @Param ( ) { id } : UUIDParamDto ) {
const dto = this . getDto ( GetUploadStatusDto , req . headers ) ;
console . log ( 'Getting upload status with dto:' , JSON . stringify ( dto ) ) ;
return this . service . getUploadStatus ( req . auth , res , id , dto ) ;
2025-09-28 18:37:16 -04:00
}
2025-09-29 03:40:24 -04:00
@Options ( )
2025-09-24 13:56:46 -04:00
@Authenticated ( { sharedLink : true , permission : Permission.AssetUpload } )
2025-09-29 18:09:06 -04:00
getUploadOptions ( @Res ( ) res : Response ) {
return this . service . getUploadOptions ( res ) ;
}
private getDto < T extends object > ( cls : new ( ) = > T , headers : IncomingHttpHeaders ) : T {
const dto = plainToInstance ( cls , headers , { excludeExtraneousValues : true } ) ;
const errors = validateSync ( dto ) ;
if ( errors . length > 0 ) {
const constraints = errors . map ( ( e ) = > ( e . constraints ? Object . values ( e . constraints ) . join ( ', ' ) : '' ) ) . join ( '; ' ) ;
console . warn ( 'Upload DTO validation failed:' , JSON . stringify ( errors , null , 2 ) ) ;
throw new BadRequestException ( constraints ) ;
}
return dto ;
2025-09-24 13:56:46 -04:00
}
}