mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
158 lines
4 KiB
TypeScript
158 lines
4 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
import { Expose, plainToInstance, Transform, Type } from 'class-transformer';
|
|
import { Equals, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, Min, ValidateIf, ValidateNested } from 'class-validator';
|
|
import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto';
|
|
import { ImmichHeader } from 'src/enum';
|
|
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
|
|
import { parseDictionary } from 'structured-headers';
|
|
|
|
export class UploadAssetDataDto {
|
|
@IsNotEmpty()
|
|
@IsString()
|
|
deviceAssetId!: string;
|
|
|
|
@IsNotEmpty()
|
|
@IsString()
|
|
deviceId!: string;
|
|
|
|
@ValidateDate()
|
|
fileCreatedAt!: Date;
|
|
|
|
@ValidateDate()
|
|
fileModifiedAt!: Date;
|
|
|
|
@Optional()
|
|
@IsString()
|
|
duration?: string;
|
|
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
filename!: string;
|
|
|
|
@ValidateBoolean({ optional: true })
|
|
isFavorite?: boolean;
|
|
|
|
@Transform(({ value }) => {
|
|
try {
|
|
const json = JSON.parse(value);
|
|
const items = Array.isArray(json) ? json : [json];
|
|
return items.map((item) => plainToInstance(AssetMetadataUpsertItemDto, item));
|
|
} catch {
|
|
throw new BadRequestException(['metadata must be valid JSON']);
|
|
}
|
|
})
|
|
@Optional()
|
|
@ValidateNested({ each: true })
|
|
@IsArray()
|
|
metadata!: AssetMetadataUpsertItemDto[];
|
|
}
|
|
|
|
export enum StructuredBoolean {
|
|
False = '?0',
|
|
True = '?1',
|
|
}
|
|
|
|
export enum UploadHeader {
|
|
UploadOffset = 'upload-offset',
|
|
ContentLength = 'content-length',
|
|
UploadLength = 'upload-length',
|
|
UploadComplete = 'upload-complete',
|
|
UploadIncomplete = 'upload-incomplete',
|
|
InteropVersion = 'upload-draft-interop-version',
|
|
ReprDigest = 'repr-digest',
|
|
}
|
|
|
|
class BaseRufhHeadersDto {
|
|
@Expose({ name: UploadHeader.InteropVersion })
|
|
@Min(3)
|
|
@IsInt()
|
|
@Type(() => Number)
|
|
version!: number;
|
|
}
|
|
|
|
export class BaseUploadHeadersDto extends BaseRufhHeadersDto {
|
|
@Expose({ name: UploadHeader.ContentLength })
|
|
@Min(0)
|
|
@IsInt()
|
|
@Type(() => Number)
|
|
contentLength!: number;
|
|
|
|
@Expose({ name: UploadHeader.UploadComplete })
|
|
@ValidateIf((o) => o.version === null || o.version! > 3)
|
|
@IsEnum(StructuredBoolean)
|
|
uploadComplete!: StructuredBoolean;
|
|
|
|
@Expose({ name: UploadHeader.UploadIncomplete })
|
|
@ValidateIf((o) => o.version !== null && o.version! <= 3)
|
|
@IsEnum(StructuredBoolean)
|
|
uploadIncomplete!: StructuredBoolean;
|
|
|
|
get isComplete(): boolean {
|
|
if (this.version <= 3) {
|
|
return this.uploadIncomplete === StructuredBoolean.False;
|
|
}
|
|
return this.uploadComplete === StructuredBoolean.True;
|
|
}
|
|
}
|
|
|
|
export class StartUploadDto extends BaseUploadHeadersDto {
|
|
@Expose({ name: ImmichHeader.AssetData })
|
|
// @ValidateNested()
|
|
// @IsObject()
|
|
@Type(() => UploadAssetDataDto)
|
|
@Transform(({ value }) => {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const json = Buffer.from(value, 'base64').toString('utf-8');
|
|
try {
|
|
return JSON.parse(json);
|
|
} catch {
|
|
throw new BadRequestException(`${ImmichHeader.AssetData} must be valid base64-encoded JSON`);
|
|
}
|
|
})
|
|
assetData!: UploadAssetDataDto;
|
|
|
|
@Expose({ name: UploadHeader.ReprDigest })
|
|
@Transform(({ value }) => {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const checksum = parseDictionary(value).get('sha')?.[0];
|
|
if (checksum instanceof ArrayBuffer && checksum.byteLength === 20) {
|
|
return Buffer.from(checksum);
|
|
}
|
|
throw new BadRequestException(`Invalid ${UploadHeader.ReprDigest} header`);
|
|
})
|
|
checksum!: Buffer;
|
|
|
|
@Expose({ name: UploadHeader.UploadLength })
|
|
@Min(0)
|
|
@IsInt()
|
|
@Type(() => Number)
|
|
uploadLength!: number;
|
|
}
|
|
|
|
export class ResumeUploadDto extends BaseUploadHeadersDto {
|
|
@Expose({ name: 'content-type' })
|
|
@ValidateIf((o) => o.version && o.version >= 6)
|
|
@Equals('application/partial-upload')
|
|
contentType!: string;
|
|
|
|
@Expose({ name: UploadHeader.UploadLength })
|
|
@Min(0)
|
|
@IsInt()
|
|
@Type(() => Number)
|
|
@Optional()
|
|
uploadLength?: number;
|
|
|
|
@Expose({ name: UploadHeader.UploadOffset })
|
|
@Min(0)
|
|
@IsInt()
|
|
@Type(() => Number)
|
|
uploadOffset!: number;
|
|
}
|
|
|
|
export class GetUploadStatusDto extends BaseRufhHeadersDto {}
|