1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-28 22:51:59 +00:00

refactor(server): move files to separate table (#11861)

This commit is contained in:
Jason Rasmussen 2024-08-19 20:03:33 -04:00 committed by GitHub
parent af3a793fe8
commit 7af6733665
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 403 additions and 210 deletions

View file

@ -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 });

View file

@ -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,

View file

@ -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;
}

View file

@ -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;

View file

@ -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,

View file

@ -5,6 +5,11 @@ export enum AssetType {
OTHER = 'OTHER',
}
export enum AssetFileType {
PREVIEW = 'preview',
THUMBNAIL = 'thumbnail',
}
export enum AlbumUserRole {
EDITOR = 'editor',
VIEWER = 'viewer',

View file

@ -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<AssetEntity[]>;
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
upsertFile(options: { assetId: string; type: AssetFileType; path: string }): Promise<void>;
}

View file

@ -0,0 +1,34 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAssetFilesTable1724101822106 implements MigrationInterface {
name = 'AddAssetFilesTable1724101822106'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
// 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"`);
}
}

View file

@ -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"

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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<AssetEntity>,
@InjectRepository(AssetFileEntity) private fileRepository: Repository<AssetFileEntity>,
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
@InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
@InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository<SmartInfoEntity>,
@ -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<AssetEntity> {
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<AssetEntity>(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<void> {
await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] });
}
}

View file

@ -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 () => {

View file

@ -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<AssetEntity> {
const asset = await this.assetRepository.getById(id);
const asset = await this.assetRepository.getById(id, { files: true });
if (!asset) {
throw new NotFoundException('Asset not found');
}

View file

@ -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,

View file

@ -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<number, AssetEntity[]> = {};
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);
}

View file

@ -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 });

View file

@ -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;
}

View file

@ -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',
});
});

View file

@ -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<JobStatus> {
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<JobStatus> {
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<JobStatus> {
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<JobStatus> {
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;

View file

@ -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({

View file

@ -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',
};
}

View file

@ -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',

View file

@ -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 {

View file

@ -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]);

View file

@ -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,
);

View file

@ -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<void>;
}
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 },

View file

@ -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) {

View file

@ -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'),

View file

@ -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,

View file

@ -42,5 +42,6 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
getAllForUserFullSync: vitest.fn(),
getChangedDeltaSync: vitest.fn(),
getDuplicates: vitest.fn(),
upsertFile: vitest.fn(),
};
};