1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +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 { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { AssetFileType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.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 { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getAssetFiles } from 'src/utils/asset.util';
export enum StorageFolder { export enum StorageFolder {
ENCODED_VIDEO = 'encoded-video', ENCODED_VIDEO = 'encoded-video',
@ -130,12 +132,14 @@ export class StorageCore {
} }
async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) { 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({ return this.moveFile({
entityId, entityId,
pathType, pathType,
oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath, oldPath: oldFile?.path || null,
newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format), newPath: StorageCore.getImagePath(asset, pathType, format),
}); });
} }
@ -285,10 +289,10 @@ export class StorageCore {
return this.assetRepository.update({ id, originalPath: newPath }); return this.assetRepository.update({ id, originalPath: newPath });
} }
case AssetPathType.PREVIEW: { case AssetPathType.PREVIEW: {
return this.assetRepository.update({ id, previewPath: newPath }); return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath });
} }
case AssetPathType.THUMBNAIL: { 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: { case AssetPathType.ENCODED_VIDEO: {
return this.assetRepository.update({ id, encodedVideoPath: newPath }); 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 { AssetEntity } from 'src/entities/asset.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { AssetType } from 'src/enum'; import { AssetType } from 'src/enum';
import { getAssetFiles } from 'src/utils/asset.util';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
export class SanitizedAssetResponseDto { export class SanitizedAssetResponseDto {
@ -111,7 +112,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
originalMimeType: mimeTypes.lookup(entity.originalFileName), originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash?.toString('base64') ?? null, thumbhash: entity.thumbhash?.toString('base64') ?? null,
localDateTime: entity.localDateTime, localDateTime: entity.localDateTime,
resized: !!entity.previewPath, resized: !!getAssetFiles(entity.files).previewFile,
duration: entity.duration ?? '0:00:00.00000', duration: entity.duration ?? '0:00:00.00000',
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false, hasMetadata: false,
@ -130,7 +131,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
originalPath: entity.originalPath, originalPath: entity.originalPath,
originalFileName: entity.originalFileName, originalFileName: entity.originalFileName,
originalMimeType: mimeTypes.lookup(entity.originalFileName), originalMimeType: mimeTypes.lookup(entity.originalFileName),
resized: !!entity.previewPath, resized: !!getAssetFiles(entity.files).previewFile,
thumbhash: entity.thumbhash?.toString('base64') ?? null, thumbhash: entity.thumbhash?.toString('base64') ?? null,
fileCreatedAt: entity.fileCreatedAt, fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt, 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 { AlbumEntity } from 'src/entities/album.entity';
import { AssetFaceEntity } from 'src/entities/asset-face.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 { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { LibraryEntity } from 'src/entities/library.entity'; import { LibraryEntity } from 'src/entities/library.entity';
@ -72,11 +73,8 @@ export class AssetEntity {
@Column() @Column()
originalPath!: string; originalPath!: string;
@Column({ type: 'varchar', nullable: true }) @OneToMany(() => AssetFileEntity, (assetFile) => assetFile.asset)
previewPath!: string | null; files!: AssetFileEntity[];
@Column({ type: 'varchar', nullable: true, default: '' })
thumbnailPath!: string | null;
@Column({ type: 'bytea', nullable: true }) @Column({ type: 'bytea', nullable: true })
thumbhash!: Buffer | null; 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 { AlbumEntity } from 'src/entities/album.entity';
import { APIKeyEntity } from 'src/entities/api-key.entity'; import { APIKeyEntity } from 'src/entities/api-key.entity';
import { AssetFaceEntity } from 'src/entities/asset-face.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 { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AuditEntity } from 'src/entities/audit.entity'; import { AuditEntity } from 'src/entities/audit.entity';
@ -32,6 +33,7 @@ export const entities = [
APIKeyEntity, APIKeyEntity,
AssetEntity, AssetEntity,
AssetFaceEntity, AssetFaceEntity,
AssetFileEntity,
AssetJobStatusEntity, AssetJobStatusEntity,
AuditEntity, AuditEntity,
ExifEntity, ExifEntity,

View file

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

View file

@ -1,7 +1,7 @@
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.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 { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { Paginated, PaginationOptions } from 'src/utils/pagination';
import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
@ -191,4 +191,5 @@ export interface IAssetRepository {
getDuplicates(options: AssetBuilderOptions): Promise<AssetEntity[]>; getDuplicates(options: AssetBuilderOptions): Promise<AssetEntity[]>;
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>; getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
getChangedDeltaSync(options: AssetDeltaSyncOptions): 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"."deviceId" AS "entity_deviceId",
"entity"."type" AS "entity_type", "entity"."type" AS "entity_type",
"entity"."originalPath" AS "entity_originalPath", "entity"."originalPath" AS "entity_originalPath",
"entity"."previewPath" AS "entity_previewPath",
"entity"."thumbnailPath" AS "entity_thumbnailPath",
"entity"."thumbhash" AS "entity_thumbhash", "entity"."thumbhash" AS "entity_thumbhash",
"entity"."encodedVideoPath" AS "entity_encodedVideoPath", "entity"."encodedVideoPath" AS "entity_encodedVideoPath",
"entity"."createdAt" AS "entity_createdAt", "entity"."createdAt" AS "entity_createdAt",
@ -59,16 +57,22 @@ SELECT
"exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."colorspace" AS "exifInfo_colorspace",
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."rating" AS "exifInfo_rating", "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 FROM
"assets" "entity" "assets" "entity"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id"
WHERE WHERE
( (
"entity"."ownerId" IN ($1) "entity"."ownerId" IN ($1)
AND "entity"."isVisible" = true AND "entity"."isVisible" = true
AND "entity"."isArchived" = false AND "entity"."isArchived" = false
AND "entity"."previewPath" IS NOT NULL
AND EXTRACT( AND EXTRACT(
DAY DAY
FROM FROM
@ -93,8 +97,6 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -129,8 +131,6 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -216,8 +216,6 @@ SELECT
"bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId",
"bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type",
"bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash",
"bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt", "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt",
@ -237,7 +235,13 @@ SELECT
"bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName", "bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName",
"bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath", "bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId", "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 FROM
"assets" "AssetEntity" "assets" "AssetEntity"
LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" 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 "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 "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 "assets" "bd93d5747511a4dad4923546c51365bf1a803774" ON "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" = "AssetEntity__AssetEntity_stack"."id"
LEFT JOIN "asset_files" "AssetEntity__AssetEntity_files" ON "AssetEntity__AssetEntity_files"."assetId" = "AssetEntity"."id"
WHERE WHERE
(("AssetEntity"."id" IN ($1))) (("AssetEntity"."id" IN ($1)))
@ -298,8 +303,6 @@ FROM
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -397,8 +400,6 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -452,8 +453,6 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -525,8 +524,6 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -581,8 +578,6 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."previewPath" AS "asset_previewPath",
"asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -603,6 +598,12 @@ SELECT
"asset"."sidecarPath" AS "asset_sidecarPath", "asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId", "asset"."stackId" AS "asset_stackId",
"asset"."duplicateId" AS "asset_duplicateId", "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"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description", "exifInfo"."description" AS "exifInfo_description",
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@ -642,8 +643,6 @@ SELECT
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt", "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@ -666,6 +665,7 @@ SELECT
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
@ -692,6 +692,7 @@ SELECT
)::timestamptz AS "timeBucket" )::timestamptz AS "timeBucket"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
@ -723,8 +724,6 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."previewPath" AS "asset_previewPath",
"asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -745,6 +744,12 @@ SELECT
"asset"."sidecarPath" AS "asset_sidecarPath", "asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId", "asset"."stackId" AS "asset_stackId",
"asset"."duplicateId" AS "asset_duplicateId", "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"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description", "exifInfo"."description" AS "exifInfo_description",
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@ -784,8 +789,6 @@ SELECT
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt", "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@ -808,6 +811,7 @@ SELECT
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
@ -841,8 +845,6 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."previewPath" AS "asset_previewPath",
"asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -863,6 +865,12 @@ SELECT
"asset"."sidecarPath" AS "asset_sidecarPath", "asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId", "asset"."stackId" AS "asset_stackId",
"asset"."duplicateId" AS "asset_duplicateId", "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"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description", "exifInfo"."description" AS "exifInfo_description",
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@ -902,8 +910,6 @@ SELECT
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt", "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@ -926,6 +932,7 @@ SELECT
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
@ -957,6 +964,7 @@ SELECT DISTINCT
c.city AS "value" c.city AS "value"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
INNER JOIN "exif" "e" ON "asset"."id" = e."assetId" INNER JOIN "exif" "e" ON "asset"."id" = e."assetId"
INNER JOIN "cities" "c" ON c.city = "e"."city" INNER JOIN "cities" "c" ON c.city = "e"."city"
WHERE WHERE
@ -987,6 +995,7 @@ SELECT DISTINCT
unnest("si"."tags") AS "value" unnest("si"."tags") AS "value"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
INNER JOIN "smart_info" "si" ON "asset"."id" = si."assetId" INNER JOIN "smart_info" "si" ON "asset"."id" = si."assetId"
INNER JOIN "random_tags" "t" ON "si"."tags" @> ARRAY[t.tag] INNER JOIN "random_tags" "t" ON "si"."tags" @> ARRAY[t.tag]
WHERE WHERE
@ -1009,8 +1018,6 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."previewPath" AS "asset_previewPath",
"asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -1031,6 +1038,12 @@ SELECT
"asset"."sidecarPath" AS "asset_sidecarPath", "asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId", "asset"."stackId" AS "asset_stackId",
"asset"."duplicateId" AS "asset_duplicateId", "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"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description", "exifInfo"."description" AS "exifInfo_description",
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@ -1065,6 +1078,7 @@ SELECT
"stack"."primaryAssetId" AS "stack_primaryAssetId" "stack"."primaryAssetId" AS "stack_primaryAssetId"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
WHERE WHERE
@ -1086,8 +1100,6 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."previewPath" AS "asset_previewPath",
"asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -1108,6 +1120,12 @@ SELECT
"asset"."sidecarPath" AS "asset_sidecarPath", "asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId", "asset"."stackId" AS "asset_stackId",
"asset"."duplicateId" AS "asset_duplicateId", "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"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description", "exifInfo"."description" AS "exifInfo_description",
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@ -1142,9 +1160,34 @@ SELECT
"stack"."primaryAssetId" AS "stack_primaryAssetId" "stack"."primaryAssetId" AS "stack_primaryAssetId"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
WHERE WHERE
"asset"."isVisible" = true "asset"."isVisible" = true
AND "asset"."ownerId" IN ($1) AND "asset"."ownerId" IN ($1)
AND "asset"."updatedAt" > $2 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"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", "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"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
@ -255,8 +253,6 @@ FROM
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -386,8 +382,6 @@ SELECT
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", "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"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",

View file

@ -14,8 +14,6 @@ FROM
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."previewPath" AS "asset_previewPath",
"asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -46,8 +44,6 @@ FROM
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt", "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@ -111,8 +107,6 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."previewPath" AS "asset_previewPath",
"asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -143,8 +137,6 @@ SELECT
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt", "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@ -353,8 +345,6 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."previewPath" AS "asset_previewPath",
"asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",

View file

@ -28,8 +28,6 @@ FROM
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", "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"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
"SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt",
@ -96,8 +94,6 @@ FROM
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt",
@ -218,8 +214,6 @@ SELECT
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", "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"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
"SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt",

View file

@ -1,11 +1,12 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; 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 { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { AssetOrder, AssetType } from 'src/enum'; import { AssetFileType, AssetOrder, AssetType } from 'src/enum';
import { import {
AssetBuilderOptions, AssetBuilderOptions,
AssetCreate, AssetCreate,
@ -59,6 +60,7 @@ const dateTrunc = (options: TimeBucketOptions) =>
export class AssetRepository implements IAssetRepository { export class AssetRepository implements IAssetRepository {
constructor( constructor(
@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>, @InjectRepository(AssetEntity) private repository: Repository<AssetEntity>,
@InjectRepository(AssetFileEntity) private fileRepository: Repository<AssetFileEntity>,
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>, @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
@InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>, @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
@InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository<SmartInfoEntity>, @InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository<SmartInfoEntity>,
@ -84,7 +86,6 @@ export class AssetRepository implements IAssetRepository {
`entity.ownerId IN (:...ownerIds) `entity.ownerId IN (:...ownerIds)
AND entity.isVisible = true AND entity.isVisible = true
AND entity.isArchived = false AND entity.isArchived = false
AND entity.previewPath IS NOT NULL
AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day
AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, 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.exifInfo', 'exifInfo')
.leftJoinAndSelect('entity.files', 'files')
.orderBy('entity.localDateTime', 'ASC') .orderBy('entity.localDateTime', 'ASC')
.getMany(); .getMany();
} }
@ -128,6 +130,7 @@ export class AssetRepository implements IAssetRepository {
stack: { stack: {
assets: true, assets: true,
}, },
files: true,
}, },
withDeleted: true, withDeleted: true,
}); });
@ -214,7 +217,7 @@ export class AssetRepository implements IAssetRepository {
} }
getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> { 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 = searchAssetBuilder(builder, options);
builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC'); builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC');
return paginatedBuilder<AssetEntity>(builder, { return paginatedBuilder<AssetEntity>(builder, {
@ -706,7 +709,11 @@ export class AssetRepository implements IAssetRepository {
} }
private getBuilder(options: AssetBuilderOptions) { 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) { if (options.assetType !== undefined) {
builder.andWhere('asset.type = :assetType', { assetType: options.assetType }); builder.andWhere('asset.type = :assetType', { assetType: options.assetType });
} }
@ -812,4 +819,9 @@ export class AssetRepository implements IAssetRepository {
.withDeleted(); .withDeleted();
return builder.getMany(); 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 { Stats } from 'node:fs';
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.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 { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
import { AssetType } from 'src/enum'; import { AssetType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
@ -150,15 +151,14 @@ const assetEntity = Object.freeze({
deviceId: 'device_id_1', deviceId: 'device_id_1',
type: AssetType.VIDEO, type: AssetType.VIDEO,
originalPath: 'fake_path/asset_1.jpeg', originalPath: 'fake_path/asset_1.jpeg',
previewPath: '',
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: 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'), updatedAt: new Date('2022-06-19T23:41:36.910Z'),
isFavorite: false, isFavorite: false,
isArchived: false, isArchived: false,
thumbnailPath: '',
encodedVideoPath: '', encodedVideoPath: '',
duration: '0:00:00.000000', duration: '0:00:00.000000',
files: [] as AssetFileEntity[],
exifInfo: { exifInfo: {
latitude: 49.533_547, latitude: 49.533_547,
longitude: 10.703_075, longitude: 10.703_075,
@ -418,7 +418,7 @@ describe(AssetMediaService.name, () => {
await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException); 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 () => { 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 { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { fromChecksum } from 'src/utils/request'; import { fromChecksum } from 'src/utils/request';
@ -238,9 +239,10 @@ export class AssetMediaService {
const asset = await this.findOrFail(id); const asset = await this.findOrFail(id);
const size = dto.size ?? AssetMediaSize.THUMBNAIL; const size = dto.size ?? AssetMediaSize.THUMBNAIL;
let filepath = asset.previewPath; const { thumbnailFile, previewFile } = getAssetFiles(asset.files);
if (size === AssetMediaSize.THUMBNAIL && asset.thumbnailPath) { let filepath = previewFile?.path;
filepath = asset.thumbnailPath; if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) {
filepath = thumbnailFile.path;
} }
if (!filepath) { if (!filepath) {
@ -460,7 +462,7 @@ export class AssetMediaService {
} }
private async findOrFail(id: string): Promise<AssetEntity> { 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) { if (!asset) {
throw new NotFoundException('Asset not found'); throw new NotFoundException('Asset not found');
} }

View file

@ -299,8 +299,8 @@ describe(AssetService.name, () => {
name: JobName.DELETE_FILES, name: JobName.DELETE_FILES,
data: { data: {
files: [ files: [
assetWithFace.thumbnailPath, '/uploads/user-id/webp/path.ext',
assetWithFace.previewPath, '/uploads/user-id/thumbs/path.jpg',
assetWithFace.encodedVideoPath, assetWithFace.encodedVideoPath,
assetWithFace.sidecarPath, assetWithFace.sidecarPath,
assetWithFace.originalPath, assetWithFace.originalPath,

View file

@ -39,7 +39,7 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStackRepository } from 'src/interfaces/stack.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.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'; import { usePagination } from 'src/utils/pagination';
export class AssetService { export class AssetService {
@ -71,9 +71,10 @@ export class AssetService {
const userIds = [auth.user.id, ...partnerIds]; const userIds = [auth.user.id, ...partnerIds];
const assets = await this.assetRepository.getByDayOfYear(userIds, dto); const assets = await this.assetRepository.getByDayOfYear(userIds, dto);
const assetsWithThumbnails = assets.filter(({ files }) => !!getAssetFiles(files).thumbnailFile);
const groups: Record<number, AssetEntity[]> = {}; const groups: Record<number, AssetEntity[]> = {};
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
for (const asset of assets) { for (const asset of assetsWithThumbnails) {
const yearsAgo = currentYear - asset.localDateTime.getFullYear(); const yearsAgo = currentYear - asset.localDateTime.getFullYear();
if (!groups[yearsAgo]) { if (!groups[yearsAgo]) {
groups[yearsAgo] = []; groups[yearsAgo] = [];
@ -126,6 +127,7 @@ export class AssetService {
exifInfo: true, exifInfo: true,
}, },
}, },
files: true,
}, },
{ {
faces: { faces: {
@ -170,6 +172,7 @@ export class AssetService {
faces: { faces: {
person: true, person: true,
}, },
files: true,
}); });
if (!asset) { if (!asset) {
throw new BadRequestException('Asset not found'); throw new BadRequestException('Asset not found');
@ -223,6 +226,7 @@ export class AssetService {
library: true, library: true,
stack: { assets: true }, stack: { assets: true },
exifInfo: true, exifInfo: true,
files: true,
}); });
if (!asset) { 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) { if (deleteOnDisk) {
files.push(asset.sidecarPath, asset.originalPath); files.push(asset.sidecarPath, asset.originalPath);
} }

View file

@ -14,7 +14,7 @@ import {
} from 'src/dtos/audit.dto'; } from 'src/dtos/audit.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; 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 { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.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 { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@Injectable() @Injectable()
@ -97,12 +98,12 @@ export class AuditService {
} }
case AssetPathType.PREVIEW: { case AssetPathType.PREVIEW: {
await this.assetRepository.update({ id, previewPath: pathValue }); await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: pathValue });
break; break;
} }
case AssetPathType.THUMBNAIL: { case AssetPathType.THUMBNAIL: {
await this.assetRepository.update({ id, thumbnailPath: pathValue }); await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: pathValue });
break; break;
} }
@ -155,7 +156,7 @@ export class AuditService {
} }
} }
const track = (filename: string | null) => { const track = (filename: string | null | undefined) => {
if (!filename) { if (!filename) {
return; return;
} }
@ -175,8 +176,9 @@ export class AuditService {
const orphans: FileReportItemDto[] = []; const orphans: FileReportItemDto[] = [];
for await (const assets of pagination) { for await (const assets of pagination) {
assetCount += assets.length; assetCount += assets.length;
for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) { for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) {
for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) { const { previewFile, thumbnailFile } = getAssetFiles(files);
for (const file of [originalPath, previewFile?.path, encodedVideoPath, thumbnailFile?.path]) {
track(file); track(file);
} }
@ -192,11 +194,11 @@ export class AuditService {
) { ) {
orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath });
} }
if (previewPath && !hasFile(thumbFiles, previewPath)) { if (previewFile && !hasFile(thumbFiles, previewFile.path)) {
orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath }); orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewFile.path });
} }
if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { if (thumbnailFile && !hasFile(thumbFiles, thumbnailFile.path)) {
orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath }); orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailFile.path });
} }
if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) {
orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath });

View file

@ -17,6 +17,7 @@ import {
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface'; import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { isDuplicateDetectionEnabled } from 'src/utils/misc'; import { isDuplicateDetectionEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@ -69,7 +70,7 @@ export class DuplicateService {
return JobStatus.SKIPPED; 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) { if (!asset) {
this.logger.error(`Asset ${id} not found`); this.logger.error(`Asset ${id} not found`);
return JobStatus.FAILED; return JobStatus.FAILED;
@ -80,7 +81,8 @@ export class DuplicateService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
if (!asset.previewPath) { const { previewFile } = getAssetFiles(asset.files);
if (!previewFile) {
this.logger.warn(`Asset ${id} is missing preview image`); this.logger.warn(`Asset ${id} is missing preview image`);
return JobStatus.FAILED; return JobStatus.FAILED;
} }

View file

@ -9,7 +9,7 @@ import {
VideoCodec, VideoCodec,
} from 'src/config'; } from 'src/config';
import { ExifEntity } from 'src/entities/exif.entity'; 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 { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@ -298,18 +298,20 @@ describe(MediaService.name, () => {
colorspace: Colorspace.SRGB, colorspace: Colorspace.SRGB,
processInvalidImages: false, 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 () => { it('should delete previous preview if different path', async () => {
const previousPreviewPath = assetStub.image.previewPath;
systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleGeneratePreview({ id: assetStub.image.id }); 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 () => { it('should generate a P3 thumbnail for a wide gamut image', async () => {
@ -330,9 +332,10 @@ describe(MediaService.name, () => {
processInvalidImages: false, processInvalidImages: false,
}, },
); );
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.upsertFile).toHaveBeenCalledWith({
id: 'asset-id', assetId: 'asset-id',
previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', type: AssetFileType.PREVIEW,
path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
}); });
}); });
@ -357,9 +360,10 @@ describe(MediaService.name, () => {
twoPass: false, twoPass: false,
}, },
); );
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.upsertFile).toHaveBeenCalledWith({
id: 'asset-id', assetId: 'asset-id',
previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', type: AssetFileType.PREVIEW,
path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
}); });
}); });
@ -384,9 +388,10 @@ describe(MediaService.name, () => {
twoPass: false, twoPass: false,
}, },
); );
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.upsertFile).toHaveBeenCalledWith({
id: 'asset-id', assetId: 'asset-id',
previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', type: AssetFileType.PREVIEW,
path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
}); });
}); });
@ -472,19 +477,21 @@ describe(MediaService.name, () => {
colorspace: Colorspace.SRGB, colorspace: Colorspace.SRGB,
processInvalidImages: false, 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 () => { it('should delete previous thumbnail if different path', async () => {
const previousThumbnailPath = assetStub.image.thumbnailPath;
systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleGenerateThumbnail({ id: assetStub.image.id }); 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, processInvalidImages: false,
}, },
); );
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.upsertFile).toHaveBeenCalledWith({
id: 'asset-id', assetId: 'asset-id',
thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', 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 { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType } from 'src/entities/move.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 { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { import {
@ -34,6 +34,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@ -72,7 +73,11 @@ export class MediaService {
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> { async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force 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); : this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
}); });
@ -80,13 +85,17 @@ export class MediaService {
const jobs: JobItem[] = []; const jobs: JobItem[] = [];
for (const asset of assets) { 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 } }); jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } });
continue; continue;
} }
if (!asset.thumbnailPath) {
if (!thumbnailFile) {
jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } }); jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } });
} }
if (!asset.thumbhash) { if (!asset.thumbhash) {
jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } }); jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } });
} }
@ -152,7 +161,7 @@ export class MediaService {
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> { async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
const { image } = await this.configCore.getConfig({ withCache: true }); 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) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
@ -182,12 +191,14 @@ export class MediaService {
return JobStatus.SKIPPED; 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}`); 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() }); await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date() });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
@ -253,7 +264,7 @@ export class MediaService {
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> { async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
const [{ image }, [asset]] = await Promise.all([ const [{ image }, [asset]] = await Promise.all([
this.configCore.getConfig({ withCache: true }), this.configCore.getConfig({ withCache: true }),
this.assetRepository.getByIds([id], { exifInfo: true }), this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
]); ]);
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
@ -268,19 +279,21 @@ export class MediaService {
return JobStatus.SKIPPED; 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}`); 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() }); await this.assetRepository.upsertJobStatus({ assetId: asset.id, thumbnailAt: new Date() });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> { async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id], { files: true });
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
@ -289,11 +302,12 @@ export class MediaService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
if (!asset.previewPath) { const { previewFile } = getAssetFiles(asset.files);
if (!previewFile) {
return JobStatus.FAILED; 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 }); await this.assetRepository.update({ id: asset.id, thumbhash });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;

View file

@ -1,6 +1,7 @@
import { defaults, SystemConfig } from 'src/config'; import { defaults, SystemConfig } from 'src/config';
import { AlbumUserEntity } from 'src/entities/album-user.entity'; 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 { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@ -333,7 +334,9 @@ describe(NotificationService.name, () => {
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' }); notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); 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({ expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.SEND_EMAIL, name: JobName.SEND_EMAIL,
data: expect.objectContaining({ data: expect.objectContaining({
@ -358,10 +361,15 @@ describe(NotificationService.name, () => {
}); });
systemMock.get.mockResolvedValue({ server: {} }); systemMock.get.mockResolvedValue({ server: {} });
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' }); 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); 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({ expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.SEND_EMAIL, name: JobName.SEND_EMAIL,
data: expect.objectContaining({ data: expect.objectContaining({
@ -389,7 +397,9 @@ describe(NotificationService.name, () => {
assetMock.getById.mockResolvedValue(assetStub.image); assetMock.getById.mockResolvedValue(assetStub.image);
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); 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({ expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.SEND_EMAIL, name: JobName.SEND_EMAIL,
data: expect.objectContaining({ 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 { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { getFilenameExtension } from 'src/utils/file'; import { getFilenameExtension } from 'src/utils/file';
import { getPreferences } from 'src/utils/preferences'; import { getPreferences } from 'src/utils/preferences';
@ -268,14 +269,15 @@ export class NotificationService {
return; return;
} }
const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId); const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId, { files: true });
if (!albumThumbnail?.thumbnailPath) { const { thumbnailFile } = getAssetFiles(albumThumbnail?.files);
if (!thumbnailFile) {
return; return;
} }
return { return {
filename: `album-thumbnail${getFilenameExtension(albumThumbnail.thumbnailPath)}`, filename: `album-thumbnail${getFilenameExtension(thumbnailFile.path)}`,
path: albumThumbnail.thumbnailPath, path: thumbnailFile.path,
cid: 'album-thumbnail', cid: 'album-thumbnail',
}; };
} }

View file

@ -716,7 +716,7 @@ describe(PersonService.name, () => {
await sut.handleDetectFaces({ id: assetStub.image.id }); await sut.handleDetectFaces({ id: assetStub.image.id });
expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( expect(machineLearningMock.detectFaces).toHaveBeenCalledWith(
'http://immich-machine-learning:3003', 'http://immich-machine-learning:3003',
assetStub.image.previewPath, '/uploads/user-id/thumbs/path.jpg',
expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }), expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
); );
expect(personMock.createFaces).not.toHaveBeenCalled(); expect(personMock.createFaces).not.toHaveBeenCalled();
@ -946,7 +946,7 @@ describe(PersonService.name, () => {
await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); 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(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs');
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
assetStub.primaryImage.originalPath, assetStub.primaryImage.originalPath,
@ -1032,7 +1032,7 @@ describe(PersonService.name, () => {
await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
assetStub.video.previewPath, '/uploads/user-id/thumbs/path.jpg',
'upload/thumbs/admin_id/pe/rs/person-1.jpeg', 'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
{ {
format: '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 { ISearchRepository } from 'src/interfaces/search.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { isFacialRecognitionEnabled } from 'src/utils/misc'; import { isFacialRecognitionEnabled } from 'src/utils/misc';
@ -333,9 +334,11 @@ export class PersonService {
faces: { faces: {
person: false, person: false,
}, },
files: true,
}; };
const [asset] = await this.assetRepository.getByIds([id], relations); 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; return JobStatus.FAILED;
} }
@ -349,11 +352,11 @@ export class PersonService {
const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces( const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces(
machineLearning.url, machineLearning.url,
asset.previewPath, previewFile.path,
machineLearning.facialRecognition, 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) { if (faces.length > 0) {
await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }); await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } });
@ -549,7 +552,10 @@ export class PersonService {
imageHeight: oldHeight, imageHeight: oldHeight,
} = face; } = face;
const asset = await this.assetRepository.getById(assetId, { exifInfo: true }); const asset = await this.assetRepository.getById(assetId, {
exifInfo: true,
files: true,
});
if (!asset) { if (!asset) {
this.logger.error(`Could not generate person thumbnail: asset ${assetId} does not exist`); this.logger.error(`Could not generate person thumbnail: asset ${assetId} does not exist`);
return JobStatus.FAILED; return JobStatus.FAILED;
@ -646,7 +652,8 @@ export class PersonService {
throw new Error(`Asset ${asset.id} dimensions are unknown`); 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`); throw new Error(`Asset ${asset.id} has no preview path`);
} }
@ -659,8 +666,8 @@ export class PersonService {
return { width, height, inputPath: asset.originalPath }; return { width, height, inputPath: asset.originalPath };
} }
const { width, height } = await this.mediaRepository.getImageDimensions(asset.previewPath); const { width, height } = await this.mediaRepository.getImageDimensions(previewFile.path);
return { width, height, inputPath: asset.previewPath }; return { width, height, inputPath: previewFile.path };
} }
private getCrop(dims: { old: ImageDimensions; new: ImageDimensions }, { x1, y1, x2, y2 }: BoundingBox): CropOptions { 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( expect(machineMock.encodeImage).toHaveBeenCalledWith(
'http://immich-machine-learning:3003', 'http://immich-machine-learning:3003',
assetStub.image.previewPath, '/uploads/user-id/thumbs/path.jpg',
expect.objectContaining({ modelName: 'ViT-B-32__openai' }), expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
); );
expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]); 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 { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@ -135,7 +136,7 @@ export class SmartInfoService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id], { files: true });
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
@ -144,13 +145,14 @@ export class SmartInfoService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
if (!asset.previewPath) { const { previewFile } = getAssetFiles(asset.files);
if (!previewFile) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const embedding = await this.machineLearning.encodeImage( const embedding = await this.machineLearning.encodeImage(
machineLearning.url, machineLearning.url,
asset.previewPath, previewFile.path,
machineLearning.clip, machineLearning.clip,
); );

View file

@ -1,7 +1,8 @@
import { AccessCore } from 'src/cores/access.core'; import { AccessCore } from 'src/cores/access.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.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 { IAccessRepository } from 'src/interfaces/access.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
@ -11,6 +12,15 @@ export interface IBulkAsset {
removeAssetIds: (id: string, assetIds: string[]) => Promise<void>; 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 ( export const addAssets = async (
auth: AuthDto, auth: AuthDto,
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, repositories: { accessRepository: IAccessRepository; repository: IBulkAsset },

View file

@ -71,7 +71,7 @@ export function searchAssetBuilder(
builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); 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)); builder.andWhere(_.omitBy(path, _.isUndefined));
if (options.originalFileName) { 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 { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { StackEntity } from 'src/entities/stack.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 { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub'; import { fileStub } from 'test/fixtures/file.stub';
import { libraryStub } from 'test/fixtures/library.stub'; import { libraryStub } from 'test/fixtures/library.stub';
import { userStub } from 'test/fixtures/user.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 => { export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => {
return { return {
id: stackId, id: stackId,
@ -29,10 +50,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: 'upload/library/IMG_123.jpg', originalPath: 'upload/library/IMG_123.jpg',
previewPath: null, files: [thumbnailFile],
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -63,10 +83,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: 'upload/library/IMG_456.jpg', originalPath: 'upload/library/IMG_456.jpg',
previewPath: '/uploads/user-id/thumbs/path.ext',
files: [previewFile],
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: null,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -101,10 +121,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext', files,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: null, thumbhash: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -136,10 +155,9 @@ export const assetStub = {
ownerId: 'admin-id', ownerId: 'admin-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
previewPath: '/uploads/admin-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
files,
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/admin-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -181,10 +199,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg', files,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -221,10 +238,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -261,10 +277,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -301,10 +316,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg', originalPath: '/data/user1/photo.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('path hash', 'utf8'), checksum: Buffer.from('path hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -341,10 +355,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -379,10 +392,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg', originalPath: '/data/user1/photo.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('path hash', 'utf8'), checksum: Buffer.from('path hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -419,10 +431,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -457,10 +468,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext', files,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2015-02-23T05:06:29.716Z'), createdAt: new Date('2015-02-23T05:06:29.716Z'),
@ -496,10 +506,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.VIDEO, type: AssetType.VIDEO,
thumbnailPath: null, files: [previewFile],
thumbhash: null, thumbhash: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -548,8 +557,22 @@ export const assetStub = {
isVisible: false, isVisible: false,
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
previewPath: '/uploads/user-id/thumbs/path.ext', files: [
thumbnailPath: '/uploads/user-id/webp/path.ext', {
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: { exifInfo: {
fileSizeInByte: 100_000, fileSizeInByte: 100_000,
timeZone: `America/New_York`, timeZone: `America/New_York`,
@ -612,10 +635,9 @@ export const assetStub = {
deviceId: 'device-id', deviceId: 'device-id',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext',
sidecarPath: null, sidecarPath: null,
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: null, files: [previewFile],
thumbhash: null, thumbhash: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-22T05:06:29.716Z'), createdAt: new Date('2023-02-22T05:06:29.716Z'),
@ -653,11 +675,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext',
thumbhash: null, thumbhash: null,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: null, files: [previewFile],
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: 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', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext',
thumbhash: null, thumbhash: null,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: null, files: [previewFile],
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: 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', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext',
thumbhash: null, thumbhash: null,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: null, files: [previewFile],
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: 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', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
previewPath: '/uploads/user-id/thumbs/path.ext',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.VIDEO, type: AssetType.VIDEO,
thumbnailPath: null, files: [previewFile],
thumbhash: null, thumbhash: null,
encodedVideoPath: '/encoded/video/path.mp4', encodedVideoPath: '/encoded/video/path.mp4',
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -794,10 +812,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg', originalPath: '/data/user1/photo.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -833,10 +850,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg', originalPath: '/data/user1/photo.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -872,10 +888,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.dng', originalPath: '/original/path.dng',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -911,10 +926,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -952,10 +966,9 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext', files,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),

View file

@ -196,7 +196,6 @@ export const sharedLinkStub = {
deviceId: 'device_id_1', deviceId: 'device_id_1',
type: AssetType.VIDEO, type: AssetType.VIDEO,
originalPath: 'fake_path/jpeg', originalPath: 'fake_path/jpeg',
previewPath: '',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
fileModifiedAt: today, fileModifiedAt: today,
fileCreatedAt: today, fileCreatedAt: today,
@ -213,7 +212,7 @@ export const sharedLinkStub = {
objects: ['a', 'b', 'c'], objects: ['a', 'b', 'c'],
asset: null as any, asset: null as any,
}, },
thumbnailPath: '', files: [],
thumbhash: null, thumbhash: null,
encodedVideoPath: '', encodedVideoPath: '',
duration: null, duration: null,

View file

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