mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
181 lines
4.7 KiB
TypeScript
181 lines
4.7 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
import { ApiProperty } from '@nestjs/swagger';
|
|
import { Expose, plainToInstance, Transform, Type } from 'class-transformer';
|
|
import { Equals, IsEmpty, IsEnum, IsInt, IsNotEmpty, IsString, Min, ValidateIf, ValidateNested } from 'class-validator';
|
|
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;
|
|
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
filename!: string;
|
|
|
|
@ValidateBoolean({ optional: true })
|
|
isFavorite?: boolean;
|
|
|
|
@Optional()
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
livePhotoVideoId?: string;
|
|
|
|
@Optional()
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
iCloudId!: string;
|
|
}
|
|
|
|
export enum StructuredBoolean {
|
|
False = '?0',
|
|
True = '?1',
|
|
}
|
|
|
|
export enum UploadHeader {
|
|
ContentLength = 'content-length',
|
|
ContentType = 'content-type',
|
|
InteropVersion = 'upload-draft-interop-version',
|
|
ReprDigest = 'repr-digest',
|
|
UploadComplete = 'upload-complete',
|
|
UploadIncomplete = 'upload-incomplete',
|
|
UploadLength = 'upload-length',
|
|
UploadOffset = 'upload-offset',
|
|
}
|
|
|
|
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()
|
|
@Transform(({ value }) => {
|
|
if (!value) {
|
|
throw new BadRequestException(`${ImmichHeader.AssetData} header is required`);
|
|
}
|
|
|
|
try {
|
|
const dict = parseDictionary(value);
|
|
return plainToInstance(UploadAssetDataDto, {
|
|
deviceAssetId: dict.get('device-asset-id')?.[0],
|
|
deviceId: dict.get('device-id')?.[0],
|
|
filename: dict.get('filename')?.[0],
|
|
duration: dict.get('duration')?.[0],
|
|
fileCreatedAt: dict.get('file-created-at')?.[0],
|
|
fileModifiedAt: dict.get('file-modified-at')?.[0],
|
|
isFavorite: dict.get('is-favorite')?.[0],
|
|
livePhotoVideoId: dict.get('live-photo-video-id')?.[0],
|
|
iCloudId: dict.get('icloud-id')?.[0],
|
|
});
|
|
} catch {
|
|
throw new BadRequestException(`${ImmichHeader.AssetData} must be a valid structured dictionary`);
|
|
}
|
|
})
|
|
assetData!: UploadAssetDataDto;
|
|
|
|
@Expose({ name: UploadHeader.ReprDigest })
|
|
@Transform(({ value }) => {
|
|
if (!value) {
|
|
throw new BadRequestException(`Missing ${UploadHeader.ReprDigest} header`);
|
|
}
|
|
|
|
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()
|
|
@Transform(({ obj, value }) => Number(value === undefined ? obj['x-upload-length'] : value))
|
|
uploadLength!: number;
|
|
|
|
@Expose({ name: UploadHeader.UploadOffset })
|
|
@IsEmpty()
|
|
uploadOffset?: string;
|
|
}
|
|
|
|
export class ResumeUploadDto extends BaseUploadHeadersDto {
|
|
@Expose({ name: UploadHeader.ContentType })
|
|
@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 {
|
|
@Expose({ name: UploadHeader.UploadComplete })
|
|
@IsEmpty()
|
|
uploadComplete?: string;
|
|
|
|
@Expose({ name: UploadHeader.UploadIncomplete })
|
|
@IsEmpty()
|
|
uploadIncomplete?: string;
|
|
|
|
@Expose({ name: UploadHeader.UploadOffset })
|
|
@IsEmpty()
|
|
uploadOffset?: string;
|
|
}
|
|
|
|
export class UploadOkDto {
|
|
@ApiProperty()
|
|
id!: string;
|
|
}
|