From 7af6733665fcf637565bbb23068a2225efa237dd Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 19 Aug 2024 20:03:33 -0400 Subject: [PATCH] refactor(server): move files to separate table (#11861) --- server/src/cores/storage.core.ts | 14 ++- server/src/dtos/asset-response.dto.ts | 5 +- server/src/entities/asset-files.entity.ts | 38 ++++++ server/src/entities/asset.entity.ts | 8 +- server/src/entities/index.ts | 2 + server/src/enum.ts | 5 + server/src/interfaces/asset.interface.ts | 3 +- .../1724101822106-AddAssetFilesTable.ts | 34 ++++++ server/src/queries/asset.repository.sql | 113 ++++++++++++------ server/src/queries/person.repository.sql | 6 - server/src/queries/search.repository.sql | 10 -- server/src/queries/shared.link.repository.sql | 6 - server/src/repositories/asset.repository.ts | 20 +++- .../src/services/asset-media.service.spec.ts | 6 +- server/src/services/asset-media.service.ts | 10 +- server/src/services/asset.service.spec.ts | 4 +- server/src/services/asset.service.ts | 11 +- server/src/services/audit.service.ts | 22 ++-- server/src/services/duplicate.service.ts | 6 +- server/src/services/media.service.spec.ts | 50 ++++---- server/src/services/media.service.ts | 44 ++++--- .../src/services/notification.service.spec.ts | 20 +++- server/src/services/notification.service.ts | 10 +- server/src/services/person.service.spec.ts | 6 +- server/src/services/person.service.ts | 21 ++-- .../src/services/smart-info.service.spec.ts | 2 +- server/src/services/smart-info.service.ts | 8 +- server/src/utils/asset.util.ts | 12 +- server/src/utils/database.ts | 2 +- server/test/fixtures/asset.stub.ts | 111 +++++++++-------- server/test/fixtures/shared-link.stub.ts | 3 +- .../repositories/asset.repository.mock.ts | 1 + 32 files changed, 403 insertions(+), 210 deletions(-) create mode 100644 server/src/entities/asset-files.entity.ts create mode 100644 server/src/migrations/1724101822106-AddAssetFilesTable.ts diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 4f386a51ef77..e20a0c658db7 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -6,6 +6,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { AssetFileType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -13,6 +14,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; export enum StorageFolder { ENCODED_VIDEO = 'encoded-video', @@ -130,12 +132,14 @@ export class StorageCore { } async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) { - const { id: entityId, previewPath, thumbnailPath } = asset; + const { id: entityId, files } = asset; + const { thumbnailFile, previewFile } = getAssetFiles(files); + const oldFile = pathType === AssetPathType.PREVIEW ? previewFile : thumbnailFile; return this.moveFile({ entityId, pathType, - oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath, - newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format), + oldPath: oldFile?.path || null, + newPath: StorageCore.getImagePath(asset, pathType, format), }); } @@ -285,10 +289,10 @@ export class StorageCore { return this.assetRepository.update({ id, originalPath: newPath }); } case AssetPathType.PREVIEW: { - return this.assetRepository.update({ id, previewPath: newPath }); + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath }); } case AssetPathType.THUMBNAIL: { - return this.assetRepository.update({ id, thumbnailPath: newPath }); + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: newPath }); } case AssetPathType.ENCODED_VIDEO: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 6ed1125253c3..332f258d4959 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -14,6 +14,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { AssetType } from 'src/enum'; +import { getAssetFiles } from 'src/utils/asset.util'; import { mimeTypes } from 'src/utils/mime-types'; export class SanitizedAssetResponseDto { @@ -111,7 +112,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalMimeType: mimeTypes.lookup(entity.originalFileName), thumbhash: entity.thumbhash?.toString('base64') ?? null, localDateTime: entity.localDateTime, - resized: !!entity.previewPath, + resized: !!getAssetFiles(entity.files).previewFile, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -130,7 +131,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalPath: entity.originalPath, originalFileName: entity.originalFileName, originalMimeType: mimeTypes.lookup(entity.originalFileName), - resized: !!entity.previewPath, + resized: !!getAssetFiles(entity.files).previewFile, thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, diff --git a/server/src/entities/asset-files.entity.ts b/server/src/entities/asset-files.entity.ts new file mode 100644 index 000000000000..a8a6ddfee102 --- /dev/null +++ b/server/src/entities/asset-files.entity.ts @@ -0,0 +1,38 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetFileType } from 'src/enum'; +import { + Column, + CreateDateColumn, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +@Unique('UQ_assetId_type', ['assetId', 'type']) +@Entity('asset_files') +export class AssetFileEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Index('IDX_asset_files_assetId') + @Column() + assetId!: string; + + @ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + asset?: AssetEntity; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; + + @Column() + type!: AssetFileType; + + @Column() + path!: string; +} diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index f4ea5eafddb9..9ebf9364d1b2 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -1,5 +1,6 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { LibraryEntity } from 'src/entities/library.entity'; @@ -72,11 +73,8 @@ export class AssetEntity { @Column() originalPath!: string; - @Column({ type: 'varchar', nullable: true }) - previewPath!: string | null; - - @Column({ type: 'varchar', nullable: true, default: '' }) - thumbnailPath!: string | null; + @OneToMany(() => AssetFileEntity, (assetFile) => assetFile.asset) + files!: AssetFileEntity[]; @Column({ type: 'bytea', nullable: true }) thumbhash!: Buffer | null; diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts index 148e2640955d..0b7ca8c3bd01 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -3,6 +3,7 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { APIKeyEntity } from 'src/entities/api-key.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { AuditEntity } from 'src/entities/audit.entity'; @@ -32,6 +33,7 @@ export const entities = [ APIKeyEntity, AssetEntity, AssetFaceEntity, + AssetFileEntity, AssetJobStatusEntity, AuditEntity, ExifEntity, diff --git a/server/src/enum.ts b/server/src/enum.ts index 4a81d54218fc..64cb1f118ab2 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -5,6 +5,11 @@ export enum AssetType { OTHER = 'OTHER', } +export enum AssetFileType { + PREVIEW = 'preview', + THUMBNAIL = 'thumbnail', +} + export enum AlbumUserRole { EDITOR = 'editor', VIEWER = 'viewer', diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index aca45f3dc770..6dd81edaefa9 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -1,7 +1,7 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetOrder, AssetType } from 'src/enum'; +import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; @@ -191,4 +191,5 @@ export interface IAssetRepository { getDuplicates(options: AssetBuilderOptions): Promise; getAllForUserFullSync(options: AssetFullSyncOptions): Promise; getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; + upsertFile(options: { assetId: string; type: AssetFileType; path: string }): Promise; } diff --git a/server/src/migrations/1724101822106-AddAssetFilesTable.ts b/server/src/migrations/1724101822106-AddAssetFilesTable.ts new file mode 100644 index 000000000000..1ed4945749dd --- /dev/null +++ b/server/src/migrations/1724101822106-AddAssetFilesTable.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAssetFilesTable1724101822106 implements MigrationInterface { + name = 'AddAssetFilesTable1724101822106' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type"), CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_asset_files_assetId" ON "asset_files" ("assetId") `); + await queryRunner.query(`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + + // preview path migration + await queryRunner.query(`INSERT INTO "asset_files" ("assetId", "type", "path") SELECT "id", 'preview', "previewPath" FROM "assets" WHERE "previewPath" IS NOT NULL AND "previewPath" != ''`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "previewPath"`); + + // thumbnail path migration + await queryRunner.query(`INSERT INTO "asset_files" ("assetId", "type", "path") SELECT "id", 'thumbnail', "thumbnailPath" FROM "assets" WHERE "thumbnailPath" IS NOT NULL AND "thumbnailPath" != ''`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "thumbnailPath"`); + } + + public async down(queryRunner: QueryRunner): Promise { + // undo preview path migration + await queryRunner.query(`ALTER TABLE "assets" ADD "previewPath" character varying`); + await queryRunner.query(`UPDATE "assets" SET "previewPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'preview'`); + + // undo thumbnail path migration + await queryRunner.query(`ALTER TABLE "assets" ADD "thumbnailPath" character varying DEFAULT ''`); + await queryRunner.query(`UPDATE "assets" SET "thumbnailPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'thumbnail'`); + + await queryRunner.query(`ALTER TABLE "asset_files" DROP CONSTRAINT "FK_e3e103a5f1d8bc8402999286040"`); + await queryRunner.query(`DROP INDEX "public"."IDX_asset_files_assetId"`); + await queryRunner.query(`DROP TABLE "asset_files"`); + } + +} diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 98fb1d6999d8..c9bd8083bb82 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -9,8 +9,6 @@ SELECT "entity"."deviceId" AS "entity_deviceId", "entity"."type" AS "entity_type", "entity"."originalPath" AS "entity_originalPath", - "entity"."previewPath" AS "entity_previewPath", - "entity"."thumbnailPath" AS "entity_thumbnailPath", "entity"."thumbhash" AS "entity_thumbhash", "entity"."encodedVideoPath" AS "entity_encodedVideoPath", "entity"."createdAt" AS "entity_createdAt", @@ -59,16 +57,22 @@ SELECT "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", "exifInfo"."rating" AS "exifInfo_rating", - "exifInfo"."fps" AS "exifInfo_fps" + "exifInfo"."fps" AS "exifInfo_fps", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path" FROM "assets" "entity" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id" WHERE ( "entity"."ownerId" IN ($1) AND "entity"."isVisible" = true AND "entity"."isArchived" = false - AND "entity"."previewPath" IS NOT NULL AND EXTRACT( DAY FROM @@ -93,8 +97,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -129,8 +131,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -216,8 +216,6 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt", @@ -237,7 +235,13 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName", "bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath", "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId", - "bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId" + "bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId", + "AssetEntity__AssetEntity_files"."id" AS "AssetEntity__AssetEntity_files_id", + "AssetEntity__AssetEntity_files"."assetId" AS "AssetEntity__AssetEntity_files_assetId", + "AssetEntity__AssetEntity_files"."createdAt" AS "AssetEntity__AssetEntity_files_createdAt", + "AssetEntity__AssetEntity_files"."updatedAt" AS "AssetEntity__AssetEntity_files_updatedAt", + "AssetEntity__AssetEntity_files"."type" AS "AssetEntity__AssetEntity_files_type", + "AssetEntity__AssetEntity_files"."path" AS "AssetEntity__AssetEntity_files_path" FROM "assets" "AssetEntity" LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" @@ -248,6 +252,7 @@ FROM LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId" LEFT JOIN "asset_stack" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."id" = "AssetEntity"."stackId" LEFT JOIN "assets" "bd93d5747511a4dad4923546c51365bf1a803774" ON "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" = "AssetEntity__AssetEntity_stack"."id" + LEFT JOIN "asset_files" "AssetEntity__AssetEntity_files" ON "AssetEntity__AssetEntity_files"."assetId" = "AssetEntity"."id" WHERE (("AssetEntity"."id" IN ($1))) @@ -298,8 +303,6 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -397,8 +400,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -452,8 +453,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -525,8 +524,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -581,8 +578,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -603,6 +598,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -642,8 +643,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -666,6 +665,7 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -692,6 +692,7 @@ SELECT )::timestamptz AS "timeBucket" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -723,8 +724,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -745,6 +744,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -784,8 +789,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -808,6 +811,7 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -841,8 +845,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -863,6 +865,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -902,8 +910,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -926,6 +932,7 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -957,6 +964,7 @@ SELECT DISTINCT c.city AS "value" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" INNER JOIN "exif" "e" ON "asset"."id" = e."assetId" INNER JOIN "cities" "c" ON c.city = "e"."city" WHERE @@ -987,6 +995,7 @@ SELECT DISTINCT unnest("si"."tags") AS "value" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" INNER JOIN "smart_info" "si" ON "asset"."id" = si."assetId" INNER JOIN "random_tags" "t" ON "si"."tags" @> ARRAY[t.tag] WHERE @@ -1009,8 +1018,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -1031,6 +1038,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1065,6 +1078,7 @@ SELECT "stack"."primaryAssetId" AS "stack_primaryAssetId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" WHERE @@ -1086,8 +1100,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -1108,6 +1120,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1142,9 +1160,34 @@ SELECT "stack"."primaryAssetId" AS "stack_primaryAssetId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" WHERE "asset"."isVisible" = true AND "asset"."ownerId" IN ($1) AND "asset"."updatedAt" > $2 + +-- AssetRepository.upsertFile +INSERT INTO + "asset_files" ( + "id", + "assetId", + "createdAt", + "updatedAt", + "type", + "path" + ) +VALUES + (DEFAULT, $1, DEFAULT, DEFAULT, $2, $3) +ON CONFLICT ("assetId", "type") DO +UPDATE +SET + "assetId" = EXCLUDED."assetId", + "type" = EXCLUDED."type", + "path" = EXCLUDED."path", + "updatedAt" = DEFAULT +RETURNING + "id", + "createdAt", + "updatedAt" diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 9b20b964d8eb..9c94232d2085 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -157,8 +157,6 @@ FROM "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", - "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", @@ -255,8 +253,6 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -386,8 +382,6 @@ SELECT "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", - "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 390aedaf3501..e9e94400ad45 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -14,8 +14,6 @@ FROM "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -46,8 +44,6 @@ FROM "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -111,8 +107,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -143,8 +137,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -353,8 +345,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 2880e6896f50..10af8d17dbdd 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -28,8 +28,6 @@ FROM "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", - "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", @@ -96,8 +94,6 @@ FROM "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", @@ -218,8 +214,6 @@ SELECT "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", - "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 80b26a67bfa9..a74451f9a5e7 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; -import { AssetOrder, AssetType } from 'src/enum'; +import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; import { AssetBuilderOptions, AssetCreate, @@ -59,6 +60,7 @@ const dateTrunc = (options: TimeBucketOptions) => export class AssetRepository implements IAssetRepository { constructor( @InjectRepository(AssetEntity) private repository: Repository, + @InjectRepository(AssetFileEntity) private fileRepository: Repository, @InjectRepository(ExifEntity) private exifRepository: Repository, @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository, @InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository, @@ -84,7 +86,6 @@ export class AssetRepository implements IAssetRepository { `entity.ownerId IN (:...ownerIds) AND entity.isVisible = true AND entity.isArchived = false - AND entity.previewPath IS NOT NULL AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, { @@ -94,6 +95,7 @@ export class AssetRepository implements IAssetRepository { }, ) .leftJoinAndSelect('entity.exifInfo', 'exifInfo') + .leftJoinAndSelect('entity.files', 'files') .orderBy('entity.localDateTime', 'ASC') .getMany(); } @@ -128,6 +130,7 @@ export class AssetRepository implements IAssetRepository { stack: { assets: true, }, + files: true, }, withDeleted: true, }); @@ -214,7 +217,7 @@ export class AssetRepository implements IAssetRepository { } getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { - let builder = this.repository.createQueryBuilder('asset'); + let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files'); builder = searchAssetBuilder(builder, options); builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC'); return paginatedBuilder(builder, { @@ -706,7 +709,11 @@ export class AssetRepository implements IAssetRepository { } private getBuilder(options: AssetBuilderOptions) { - const builder = this.repository.createQueryBuilder('asset').where('asset.isVisible = true'); + const builder = this.repository + .createQueryBuilder('asset') + .where('asset.isVisible = true') + .leftJoinAndSelect('asset.files', 'files'); + if (options.assetType !== undefined) { builder.andWhere('asset.type = :assetType', { assetType: options.assetType }); } @@ -812,4 +819,9 @@ export class AssetRepository implements IAssetRepository { .withDeleted(); return builder.getMany(); } + + @GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] }) + async upsertFile({ assetId, type, path }: { assetId: string; type: AssetFileType; path: string }): Promise { + await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] }); + } } diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 978f98cf10f8..2f5192d84fcf 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -2,6 +2,7 @@ import { BadRequestException, NotFoundException, UnauthorizedException } from '@ import { Stats } from 'node:fs'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; @@ -150,15 +151,14 @@ const assetEntity = Object.freeze({ deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/asset_1.jpeg', - previewPath: '', fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), updatedAt: new Date('2022-06-19T23:41:36.910Z'), isFavorite: false, isArchived: false, - thumbnailPath: '', encodedVideoPath: '', duration: '0:00:00.000000', + files: [] as AssetFileEntity[], exifInfo: { latitude: 49.533_547, longitude: 10.703_075, @@ -418,7 +418,7 @@ describe(AssetMediaService.name, () => { await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException); - expect(assetMock.getById).toHaveBeenCalledWith('asset-1'); + expect(assetMock.getById).toHaveBeenCalledWith('asset-1', { files: true }); }); it('should download a file', async () => { diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index b8a43b34ec22..b66b0607b390 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -36,6 +36,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { fromChecksum } from 'src/utils/request'; @@ -238,9 +239,10 @@ export class AssetMediaService { const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; - let filepath = asset.previewPath; - if (size === AssetMediaSize.THUMBNAIL && asset.thumbnailPath) { - filepath = asset.thumbnailPath; + const { thumbnailFile, previewFile } = getAssetFiles(asset.files); + let filepath = previewFile?.path; + if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) { + filepath = thumbnailFile.path; } if (!filepath) { @@ -460,7 +462,7 @@ export class AssetMediaService { } private async findOrFail(id: string): Promise { - const asset = await this.assetRepository.getById(id); + const asset = await this.assetRepository.getById(id, { files: true }); if (!asset) { throw new NotFoundException('Asset not found'); } diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index f79b2819ff68..3ac7aa1c718f 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -299,8 +299,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetWithFace.thumbnailPath, - assetWithFace.previewPath, + '/uploads/user-id/webp/path.ext', + '/uploads/user-id/thumbs/path.jpg', assetWithFace.encodedVideoPath, assetWithFace.sidecarPath, assetWithFace.originalPath, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 94a3ba16038b..e9aefce91083 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -39,7 +39,7 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; -import { getMyPartnerIds } from 'src/utils/asset.util'; +import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; export class AssetService { @@ -71,9 +71,10 @@ export class AssetService { const userIds = [auth.user.id, ...partnerIds]; const assets = await this.assetRepository.getByDayOfYear(userIds, dto); + const assetsWithThumbnails = assets.filter(({ files }) => !!getAssetFiles(files).thumbnailFile); const groups: Record = {}; const currentYear = new Date().getFullYear(); - for (const asset of assets) { + for (const asset of assetsWithThumbnails) { const yearsAgo = currentYear - asset.localDateTime.getFullYear(); if (!groups[yearsAgo]) { groups[yearsAgo] = []; @@ -126,6 +127,7 @@ export class AssetService { exifInfo: true, }, }, + files: true, }, { faces: { @@ -170,6 +172,7 @@ export class AssetService { faces: { person: true, }, + files: true, }); if (!asset) { throw new BadRequestException('Asset not found'); @@ -223,6 +226,7 @@ export class AssetService { library: true, stack: { assets: true }, exifInfo: true, + files: true, }); if (!asset) { @@ -260,7 +264,8 @@ export class AssetService { } } - const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath]; + const { thumbnailFile, previewFile } = getAssetFiles(asset.files); + const files = [thumbnailFile?.path, previewFile?.path, asset.encodedVideoPath]; if (deleteOnDisk) { files.push(asset.sidecarPath, asset.originalPath); } diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index 225bd1106176..734ed9b7c353 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -14,7 +14,7 @@ import { } from 'src/dtos/audit.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; -import { DatabaseAction, Permission } from 'src/enum'; +import { AssetFileType, DatabaseAction, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; @@ -24,6 +24,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; @Injectable() @@ -97,12 +98,12 @@ export class AuditService { } case AssetPathType.PREVIEW: { - await this.assetRepository.update({ id, previewPath: pathValue }); + await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: pathValue }); break; } case AssetPathType.THUMBNAIL: { - await this.assetRepository.update({ id, thumbnailPath: pathValue }); + await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: pathValue }); break; } @@ -155,7 +156,7 @@ export class AuditService { } } - const track = (filename: string | null) => { + const track = (filename: string | null | undefined) => { if (!filename) { return; } @@ -175,8 +176,9 @@ export class AuditService { const orphans: FileReportItemDto[] = []; for await (const assets of pagination) { assetCount += assets.length; - for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) { - for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) { + for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) { + const { previewFile, thumbnailFile } = getAssetFiles(files); + for (const file of [originalPath, previewFile?.path, encodedVideoPath, thumbnailFile?.path]) { track(file); } @@ -192,11 +194,11 @@ export class AuditService { ) { orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); } - if (previewPath && !hasFile(thumbFiles, previewPath)) { - orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath }); + if (previewFile && !hasFile(thumbFiles, previewFile.path)) { + orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewFile.path }); } - if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { - orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath }); + if (thumbnailFile && !hasFile(thumbFiles, thumbnailFile.path)) { + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailFile.path }); } if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 70852a538197..35a1a7325bb2 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -17,6 +17,7 @@ import { import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { isDuplicateDetectionEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -69,7 +70,7 @@ export class DuplicateService { return JobStatus.SKIPPED; } - const asset = await this.assetRepository.getById(id, { smartSearch: true }); + const asset = await this.assetRepository.getById(id, { files: true, smartSearch: true }); if (!asset) { this.logger.error(`Asset ${id} not found`); return JobStatus.FAILED; @@ -80,7 +81,8 @@ export class DuplicateService { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { this.logger.warn(`Asset ${id} is missing preview image`); return JobStatus.FAILED; } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index d9d5948cead1..634cd790ebd0 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -9,7 +9,7 @@ import { VideoCodec, } from 'src/config'; import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetType } from 'src/enum'; +import { AssetFileType, AssetType } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -298,18 +298,20 @@ describe(MediaService.name, () => { colorspace: Colorspace.SRGB, processInvalidImages: false, }); - expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath }); + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: previewPath, + }); }); it('should delete previous preview if different path', async () => { - const previousPreviewPath = assetStub.image.previewPath; - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleGeneratePreview({ id: assetStub.image.id }); - expect(storageMock.unlink).toHaveBeenCalledWith(previousPreviewPath); + expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); }); it('should generate a P3 thumbnail for a wide gamut image', async () => { @@ -330,9 +332,10 @@ describe(MediaService.name, () => { processInvalidImages: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -357,9 +360,10 @@ describe(MediaService.name, () => { twoPass: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -384,9 +388,10 @@ describe(MediaService.name, () => { twoPass: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -472,19 +477,21 @@ describe(MediaService.name, () => { colorspace: Colorspace.SRGB, processInvalidImages: false, }); - expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath }); + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: thumbnailPath, + }); }, ); it('should delete previous thumbnail if different path', async () => { - const previousThumbnailPath = assetStub.image.thumbnailPath; - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - expect(storageMock.unlink).toHaveBeenCalledWith(previousThumbnailPath); + expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/webp/path.ext'); }); }); @@ -504,9 +511,10 @@ describe(MediaService.name, () => { processInvalidImages: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index ff77cbb34ef4..b48d00a7a818 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -15,7 +15,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; -import { AssetType } from 'src/enum'; +import { AssetFileType, AssetType } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { @@ -34,6 +34,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; import { usePagination } from 'src/utils/pagination'; @@ -72,7 +73,11 @@ export class MediaService { async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination, { isVisible: true, withDeleted: true, withArchived: true }) + ? this.assetRepository.getAll(pagination, { + isVisible: true, + withDeleted: true, + withArchived: true, + }) : this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL); }); @@ -80,13 +85,17 @@ export class MediaService { const jobs: JobItem[] = []; for (const asset of assets) { - if (!asset.previewPath || force) { + const { previewFile, thumbnailFile } = getAssetFiles(asset.files); + + if (!previewFile || force) { jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } }); continue; } - if (!asset.thumbnailPath) { + + if (!thumbnailFile) { jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } }); } + if (!asset.thumbhash) { jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } }); } @@ -152,7 +161,7 @@ export class MediaService { async handleAssetMigration({ id }: IEntityJob): Promise { const { image } = await this.configCore.getConfig({ withCache: true }); - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -182,12 +191,14 @@ export class MediaService { return JobStatus.SKIPPED; } - if (asset.previewPath && asset.previewPath !== previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (previewFile && previewFile.path !== previewPath) { this.logger.debug(`Deleting old preview for asset ${asset.id}`); - await this.storageRepository.unlink(asset.previewPath); + await this.storageRepository.unlink(previewFile.path); } - await this.assetRepository.update({ id: asset.id, previewPath }); + await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.PREVIEW, path: previewPath }); + await this.assetRepository.update({ id: asset.id, updatedAt: new Date() }); await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date() }); return JobStatus.SUCCESS; @@ -253,7 +264,7 @@ export class MediaService { async handleGenerateThumbnail({ id }: IEntityJob): Promise { const [{ image }, [asset]] = await Promise.all([ this.configCore.getConfig({ withCache: true }), - this.assetRepository.getByIds([id], { exifInfo: true }), + this.assetRepository.getByIds([id], { exifInfo: true, files: true }), ]); if (!asset) { return JobStatus.FAILED; @@ -268,19 +279,21 @@ export class MediaService { return JobStatus.SKIPPED; } - if (asset.thumbnailPath && asset.thumbnailPath !== thumbnailPath) { + const { thumbnailFile } = getAssetFiles(asset.files); + if (thumbnailFile && thumbnailFile.path !== thumbnailPath) { this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`); - await this.storageRepository.unlink(asset.thumbnailPath); + await this.storageRepository.unlink(thumbnailFile.path); } - await this.assetRepository.update({ id: asset.id, thumbnailPath }); + await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: thumbnailPath }); + await this.assetRepository.update({ id: asset.id, updatedAt: new Date() }); await this.assetRepository.upsertJobStatus({ assetId: asset.id, thumbnailAt: new Date() }); return JobStatus.SUCCESS; } async handleGenerateThumbhash({ id }: IEntityJob): Promise { - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -289,11 +302,12 @@ export class MediaService { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { return JobStatus.FAILED; } - const thumbhash = await this.mediaRepository.generateThumbhash(asset.previewPath); + const thumbhash = await this.mediaRepository.generateThumbhash(previewFile.path); await this.assetRepository.update({ id: asset.id, thumbhash }); return JobStatus.SUCCESS; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 74d2a12127db..bcce902e91dc 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,6 +1,7 @@ import { defaults, SystemConfig } from 'src/config'; import { AlbumUserEntity } from 'src/entities/album-user.entity'; -import { UserMetadataKey } from 'src/enum'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; +import { AssetFileType, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -333,7 +334,9 @@ describe(NotificationService.name, () => { notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' }); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ @@ -358,10 +361,15 @@ describe(NotificationService.name, () => { }); systemMock.get.mockResolvedValue({ server: {} }); notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' }); - assetMock.getById.mockResolvedValue({ ...assetStub.image, thumbnailPath: 'path-to-thumb.jpg' }); + assetMock.getById.mockResolvedValue({ + ...assetStub.image, + files: [{ assetId: 'asset-id', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' } as AssetFileEntity], + }); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ @@ -389,7 +397,9 @@ describe(NotificationService.name, () => { assetMock.getById.mockResolvedValue(assetStub.image); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 80abc4ca983d..31701013b70f 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -21,6 +21,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; import { getPreferences } from 'src/utils/preferences'; @@ -268,14 +269,15 @@ export class NotificationService { return; } - const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId); - if (!albumThumbnail?.thumbnailPath) { + const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId, { files: true }); + const { thumbnailFile } = getAssetFiles(albumThumbnail?.files); + if (!thumbnailFile) { return; } return { - filename: `album-thumbnail${getFilenameExtension(albumThumbnail.thumbnailPath)}`, - path: albumThumbnail.thumbnailPath, + filename: `album-thumbnail${getFilenameExtension(thumbnailFile.path)}`, + path: thumbnailFile.path, cid: 'album-thumbnail', }; } diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 70e043cc7f3a..f8608243ae92 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -716,7 +716,7 @@ describe(PersonService.name, () => { await sut.handleDetectFaces({ id: assetStub.image.id }); expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', - assetStub.image.previewPath, + '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }), ); expect(personMock.createFaces).not.toHaveBeenCalled(); @@ -946,7 +946,7 @@ describe(PersonService.name, () => { await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); - expect(assetMock.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true }); + expect(assetMock.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true, files: true }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs'); expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( assetStub.primaryImage.originalPath, @@ -1032,7 +1032,7 @@ describe(PersonService.name, () => { await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - assetStub.video.previewPath, + '/uploads/user-id/thumbs/path.jpg', 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', { format: 'jpeg', diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 6d536f4bf84d..3fc34d8b1561 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -50,6 +50,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf import { ISearchRepository } from 'src/interfaces/search.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { isFacialRecognitionEnabled } from 'src/utils/misc'; @@ -333,9 +334,11 @@ export class PersonService { faces: { person: false, }, + files: true, }; const [asset] = await this.assetRepository.getByIds([id], relations); - if (!asset || !asset.previewPath || asset.faces?.length > 0) { + const { previewFile } = getAssetFiles(asset.files); + if (!asset || !previewFile || asset.faces?.length > 0) { return JobStatus.FAILED; } @@ -349,11 +352,11 @@ export class PersonService { const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces( machineLearning.url, - asset.previewPath, + previewFile.path, machineLearning.facialRecognition, ); - this.logger.debug(`${faces.length} faces detected in ${asset.previewPath}`); + this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`); if (faces.length > 0) { await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }); @@ -549,7 +552,10 @@ export class PersonService { imageHeight: oldHeight, } = face; - const asset = await this.assetRepository.getById(assetId, { exifInfo: true }); + const asset = await this.assetRepository.getById(assetId, { + exifInfo: true, + files: true, + }); if (!asset) { this.logger.error(`Could not generate person thumbnail: asset ${assetId} does not exist`); return JobStatus.FAILED; @@ -646,7 +652,8 @@ export class PersonService { throw new Error(`Asset ${asset.id} dimensions are unknown`); } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { throw new Error(`Asset ${asset.id} has no preview path`); } @@ -659,8 +666,8 @@ export class PersonService { return { width, height, inputPath: asset.originalPath }; } - const { width, height } = await this.mediaRepository.getImageDimensions(asset.previewPath); - return { width, height, inputPath: asset.previewPath }; + const { width, height } = await this.mediaRepository.getImageDimensions(previewFile.path); + return { width, height, inputPath: previewFile.path }; } private getCrop(dims: { old: ImageDimensions; new: ImageDimensions }, { x1, y1, x2, y2 }: BoundingBox): CropOptions { diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 278e06d287db..97d22da9b8a4 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -318,7 +318,7 @@ describe(SmartInfoService.name, () => { expect(machineMock.encodeImage).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', - assetStub.image.previewPath, + '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 883f320abf50..d57b5fb54ff8 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -18,6 +18,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -135,7 +136,7 @@ export class SmartInfoService { return JobStatus.SKIPPED; } - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -144,13 +145,14 @@ export class SmartInfoService { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { return JobStatus.FAILED; } const embedding = await this.machineLearning.encodeImage( machineLearning.url, - asset.previewPath, + previewFile.path, machineLearning.clip, ); diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index aa77a0b14431..31f708611ddb 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,7 +1,8 @@ import { AccessCore } from 'src/cores/access.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { Permission } from 'src/enum'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; +import { AssetFileType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; @@ -11,6 +12,15 @@ export interface IBulkAsset { removeAssetIds: (id: string, assetIds: string[]) => Promise; } +const getFileByType = (files: AssetFileEntity[] | undefined, type: AssetFileType) => { + return (files || []).find((file) => file.type === type); +}; + +export const getAssetFiles = (files?: AssetFileEntity[]) => ({ + previewFile: getFileByType(files, AssetFileType.PREVIEW), + thumbnailFile: getFileByType(files, AssetFileType.THUMBNAIL), +}); + export const addAssets = async ( auth: AuthDto, repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 21a40ffcc876..f3232eb78bb2 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -71,7 +71,7 @@ export function searchAssetBuilder( builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); } - const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'previewPath', 'thumbnailPath']); + const path = _.pick(options, ['encodedVideoPath', 'originalPath']); builder.andWhere(_.omitBy(path, _.isUndefined)); if (options.originalFileName) { diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 23df5e4f5621..b8c7e06d8218 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,12 +1,33 @@ +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { StackEntity } from 'src/entities/stack.entity'; -import { AssetType } from 'src/enum'; +import { AssetFileType, AssetType } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { libraryStub } from 'test/fixtures/library.stub'; import { userStub } from 'test/fixtures/user.stub'; +const previewFile: AssetFileEntity = { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: '/uploads/user-id/thumbs/path.jpg', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), +}; + +const thumbnailFile: AssetFileEntity = { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: '/uploads/user-id/webp/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), +}; + +const files: AssetFileEntity[] = [previewFile, thumbnailFile]; + export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => { return { id: stackId, @@ -29,10 +50,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_123.jpg', - previewPath: null, + files: [thumbnailFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -63,10 +83,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_456.jpg', - previewPath: '/uploads/user-id/thumbs/path.ext', + + files: [previewFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -101,10 +121,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -136,10 +155,9 @@ export const assetStub = { ownerId: 'admin-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/admin-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), + files, type: AssetType.IMAGE, - thumbnailPath: '/uploads/admin-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -181,10 +199,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -221,10 +238,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -261,10 +277,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -301,10 +316,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('path hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -341,10 +355,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -379,10 +392,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('path hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -419,10 +431,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -457,10 +468,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2015-02-23T05:06:29.716Z'), @@ -496,10 +506,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -548,8 +557,22 @@ export const assetStub = { isVisible: false, fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - previewPath: '/uploads/user-id/thumbs/path.ext', - thumbnailPath: '/uploads/user-id/webp/path.ext', + files: [ + { + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: '/uploads/user-id/thumbs/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + }, + { + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: '/uploads/user-id/webp/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + }, + ], exifInfo: { fileSizeInByte: 100_000, timeZone: `America/New_York`, @@ -612,10 +635,9 @@ export const assetStub = { deviceId: 'device-id', checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', sidecarPath: null, type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-22T05:06:29.716Z'), @@ -653,11 +675,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -687,11 +708,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -722,11 +742,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -758,10 +777,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: '/encoded/video/path.mp4', createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -794,10 +812,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -833,10 +850,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -872,10 +888,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.dng', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -911,10 +926,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -952,10 +966,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 8a5cc17d4ffa..9ea252b5f7ec 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -196,7 +196,6 @@ export const sharedLinkStub = { deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/jpeg', - previewPath: '', checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today, fileCreatedAt: today, @@ -213,7 +212,7 @@ export const sharedLinkStub = { objects: ['a', 'b', 'c'], asset: null as any, }, - thumbnailPath: '', + files: [], thumbhash: null, encodedVideoPath: '', duration: null, diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index f1091c041f8b..9320639b9377 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -42,5 +42,6 @@ export const newAssetRepositoryMock = (): Mocked => { getAllForUserFullSync: vitest.fn(), getChangedDeltaSync: vitest.fn(), getDuplicates: vitest.fn(), + upsertFile: vitest.fn(), }; };