From 9922c8de596e8dfb318b84c2f8134f5adea485a2 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Wed, 5 Mar 2025 15:00:37 +0000 Subject: [PATCH] fix: storage template failure after re-upload and previous fail (#16611) fix: storage template breaks when files are re-uploaded after a move failure --- server/src/entities/move.entity.ts | 2 +- .../1741179334403-MoveHistoryUuidEntityId.ts | 26 +++++++++++++++++ server/src/queries/move.repository.sql | 19 +++++++++++++ server/src/repositories/move.repository.ts | 28 +++++++++++++++++-- .../src/services/storage-template.service.ts | 7 +++++ .../test/repositories/move.repository.mock.ts | 2 ++ 6 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 server/src/migrations/1741179334403-MoveHistoryUuidEntityId.ts diff --git a/server/src/entities/move.entity.ts b/server/src/entities/move.entity.ts index 5cdef5d22e..7a998eaebe 100644 --- a/server/src/entities/move.entity.ts +++ b/server/src/entities/move.entity.ts @@ -10,7 +10,7 @@ export class MoveEntity { @PrimaryGeneratedColumn('uuid') id!: string; - @Column({ type: 'varchar' }) + @Column({ type: 'uuid' }) entityId!: string; @Column({ type: 'varchar' }) diff --git a/server/src/migrations/1741179334403-MoveHistoryUuidEntityId.ts b/server/src/migrations/1741179334403-MoveHistoryUuidEntityId.ts new file mode 100644 index 0000000000..449272341c --- /dev/null +++ b/server/src/migrations/1741179334403-MoveHistoryUuidEntityId.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MoveHistoryUuidEntityId1741179334403 implements MigrationInterface { + name = 'MoveHistoryUuidEntityId1741179334403'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "move_history" ALTER COLUMN "entityId" TYPE uuid USING "entityId"::uuid;`); + await queryRunner.query(`delete from "move_history" + where + "move_history"."entityId" not in ( + select + "id" + from + "assets" + where + "assets"."id" = "move_history"."entityId" + ) + and "move_history"."pathType" = 'original' + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "move_history" ALTER COLUMN "entityId" TYPE character varying`); + } +} + diff --git a/server/src/queries/move.repository.sql b/server/src/queries/move.repository.sql index e51f2829df..a65c7a8b85 100644 --- a/server/src/queries/move.repository.sql +++ b/server/src/queries/move.repository.sql @@ -15,3 +15,22 @@ where "id" = $1 returning * + +-- MoveRepository.cleanMoveHistory +delete from "move_history" +where + "move_history"."entityId" not in ( + select + "id" + from + "assets" + where + "assets"."id" = "move_history"."entityId" + ) + and "move_history"."pathType" = 'original' + +-- MoveRepository.cleanMoveHistorySingle +delete from "move_history" +where + "move_history"."pathType" = 'original' + and "entityId" = $1 diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts index c46259fa9b..706e23cef7 100644 --- a/server/src/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, Updateable } from 'kysely'; +import { Insertable, Kysely, sql, Updateable } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DB, MoveHistory } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { MoveEntity } from 'src/entities/move.entity'; -import { PathType } from 'src/enum'; +import { AssetPathType, PathType } from 'src/enum'; export type MoveCreate = Pick & Partial; @@ -47,4 +47,28 @@ export class MoveRepository { .returningAll() .executeTakeFirstOrThrow() as unknown as Promise; } + + @GenerateSql() + async cleanMoveHistory(): Promise { + await this.db + .deleteFrom('move_history') + .where((eb) => + eb( + 'move_history.entityId', + 'not in', + eb.selectFrom('assets').select('id').whereRef('assets.id', '=', 'move_history.entityId'), + ), + ) + .where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) + .execute(); + } + + @GenerateSql() + async cleanMoveHistorySingle(assetId: string): Promise { + await this.db + .deleteFrom('move_history') + .where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) + .where('entityId', '=', assetId) + .execute(); + } } diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 6a1548ff20..ea8db9857d 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -152,6 +152,7 @@ export class StorageTemplateService extends BaseService { this.logger.log('Storage template migration disabled, skipping'); return JobStatus.SKIPPED; } + await this.moveRepository.cleanMoveHistory(); const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => this.assetRepository.getAll(pagination, { withExif: true, withArchived: true }), ); @@ -175,6 +176,12 @@ export class StorageTemplateService extends BaseService { return JobStatus.SUCCESS; } + @OnEvent({ name: 'asset.delete' }) + async handleMoveHistoryCleanup({ assetId }: ArgOf<'asset.delete'>) { + this.logger.debug(`Cleaning up move history for asset ${assetId}`); + await this.moveRepository.cleanMoveHistorySingle(assetId); + } + async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) { if (asset.isExternal || StorageCore.isAndroidMotionPath(asset.originalPath)) { // External assets are not affected by storage template diff --git a/server/test/repositories/move.repository.mock.ts b/server/test/repositories/move.repository.mock.ts index cf304b591e..88dfa29d4f 100644 --- a/server/test/repositories/move.repository.mock.ts +++ b/server/test/repositories/move.repository.mock.ts @@ -8,5 +8,7 @@ export const newMoveRepositoryMock = (): Mocked