import { applyDecorators } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsOptional, IsString, IsUUID, ValidateIf, ValidationOptions, } from 'class-validator'; import { CronJob } from 'cron'; import { basename, extname } from 'node:path'; import sanitize from 'sanitize-filename'; export enum CacheControl { PRIVATE_WITH_CACHE = 'private_with_cache', PRIVATE_WITHOUT_CACHE = 'private_without_cache', NONE = 'none', } export class ImmichFileResponse { public readonly path!: string; public readonly contentType!: string; public readonly cacheControl!: CacheControl; constructor(response: ImmichFileResponse) { Object.assign(this, response); } } export interface OpenGraphTags { title: string; description: string; imageUrl?: string; } export type Options = { optional?: boolean; each?: boolean; }; export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) { return applyDecorators( IsUUID('4', { each }), ApiProperty({ format: 'uuid' }), optional ? Optional() : IsNotEmpty(), each ? IsArray() : IsString(), ); } export function validateCronExpression(expression: string) { try { new CronJob(expression, () => {}); } catch (error) { return false; } return true; } interface IValue { value?: string; } export const QueryBoolean = ({ optional }: { optional?: boolean }) => { const decorators = [IsBoolean(), Transform(toBoolean)]; if (optional) { decorators.push(Optional()); } return applyDecorators(...decorators); }; export const QueryDate = ({ optional }: { optional?: boolean }) => { const decorators = [IsDate(), Type(() => Date)]; if (optional) { decorators.push(Optional()); } return applyDecorators(...decorators); }; export const toBoolean = ({ value }: IValue) => { if (value == 'true') { return true; } else if (value == 'false') { return false; } return value; }; export const toEmail = ({ value }: IValue) => value?.toLowerCase(); export const toSanitized = ({ value }: IValue) => sanitize((value || '').replace(/\./g, '')); export function getFileNameWithoutExtension(path: string): string { return basename(path, extname(path)); } export function getLivePhotoMotionFilename(stillName: string, motionName: string) { return getFileNameWithoutExtension(stillName) + extname(motionName); } const KiB = Math.pow(1024, 1); const MiB = Math.pow(1024, 2); const GiB = Math.pow(1024, 3); const TiB = Math.pow(1024, 4); const PiB = Math.pow(1024, 5); export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB }; export function asHumanReadable(bytes: number, precision = 1): string { const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; let magnitude = 0; let remainder = bytes; while (remainder >= 1024) { if (magnitude + 1 < units.length) { magnitude++; remainder /= 1024; } else { break; } } return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`; } export interface PaginationOptions { take: number; skip?: number; } export interface PaginationResult { items: T[]; hasNextPage: boolean; } export type Paginated = Promise>; export async function* usePagination( pageSize: number, getNextPage: (pagination: PaginationOptions) => Paginated, ) { let hasNextPage = true; for (let skip = 0; hasNextPage; skip += pageSize) { const result = await getNextPage({ take: pageSize, skip }); hasNextPage = result.hasNextPage; yield result.items; } } export interface OptionalOptions extends ValidationOptions { nullable?: boolean; } /** * Checks if value is missing and if so, ignores all validators. * * @param validationOptions {@link OptionalOptions} * * @see IsOptional exported from `class-validator. */ // https://stackoverflow.com/a/71353929 export function Optional({ nullable, ...validationOptions }: OptionalOptions = {}) { if (nullable === true) { return IsOptional(validationOptions); } return ValidateIf((obj: any, v: any) => v !== undefined, validationOptions); } // NOTE: The following Set utils have been added here, to easily determine where they are used. // They should be replaced with native Set operations, when they are added to the language. // Proposal reference: https://github.com/tc39/proposal-set-methods export const setUnion = (...sets: Set[]): Set => { const union = new Set(sets[0]); for (const set of sets.slice(1)) { for (const elem of set) { union.add(elem); } } return union; }; export const setDifference = (setA: Set, ...sets: Set[]): Set => { const difference = new Set(setA); for (const set of sets) { for (const elem of set) { difference.delete(elem); } } return difference; }; export const setIsSuperset = (set: Set, subset: Set): boolean => { for (const elem of subset) { if (!set.has(elem)) { return false; } } return true; }; export const setIsEqual = (setA: Set, setB: Set): boolean => { return setA.size === setB.size && setIsSuperset(setA, setB); };