refactor: dedicated query for asset migration job (#17631)

This commit is contained in:
Daniel Dietzler 2025-04-15 21:49:15 +02:00 committed by GitHub
parent 26f0ea4cb5
commit 21becbf1b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 54 additions and 9 deletions

View File

@ -1,7 +1,7 @@
import { randomUUID } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
import { APP_MEDIA_LOCATION } from 'src/constants';
import { AssetEntity } from 'src/entities/asset.entity';
import { StorageAsset } from 'src/database';
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
import { AssetRepository } from 'src/repositories/asset.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
@ -28,6 +28,8 @@ export interface MoveRequest {
export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL | AssetPathType.FULLSIZE;
export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDEO;
type ThumbnailPathEntity = { id: string; ownerId: string };
let instance: StorageCore | null;
export class StorageCore {
@ -84,19 +86,19 @@ export class StorageCore {
return join(APP_MEDIA_LOCATION, folder);
}
static getPersonThumbnailPath(person: { id: string; ownerId: string }) {
static getPersonThumbnailPath(person: ThumbnailPathEntity) {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
}
static getImagePath(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: ImageFormat) {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`);
}
static getEncodedVideoPath(asset: AssetEntity) {
static getEncodedVideoPath(asset: ThumbnailPathEntity) {
return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}.mp4`);
}
static getAndroidMotionPath(asset: AssetEntity, uuid: string) {
static getAndroidMotionPath(asset: ThumbnailPathEntity, uuid: string) {
return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${uuid}-MP.mp4`);
}
@ -114,7 +116,7 @@ export class StorageCore {
return normalizedPath.startsWith(normalizedAppMediaLocation);
}
async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) {
async moveAssetImage(asset: StorageAsset, pathType: GeneratedImageType, format: ImageFormat) {
const { id: entityId, files } = asset;
const oldFile = getAssetFile(files, pathType);
return this.moveFile({
@ -125,7 +127,7 @@ export class StorageCore {
});
}
async moveAssetVideo(asset: AssetEntity) {
async moveAssetVideo(asset: StorageAsset) {
return this.moveFile({
entityId: asset.id,
pathType: AssetPathType.ENCODED_VIDEO,

View File

@ -121,6 +121,13 @@ export type UserAdmin = User & {
metadata: UserMetadataItem[];
};
export type StorageAsset = {
id: string;
ownerId: string;
files: AssetFile[];
encodedVideoPath: string | null;
};
export type Asset = {
createdAt: Date;
updatedAt: Date;

View File

@ -90,6 +90,31 @@ where
or "assets"."thumbhash" is null
)
-- AssetJobRepository.getForMigrationJob
select
"assets"."id",
"assets"."ownerId",
"assets"."encodedVideoPath",
(
select
coalesce(json_agg(agg), '[]')
from
(
select
"asset_files"."id",
"asset_files"."path",
"asset_files"."type"
from
"asset_files"
where
"asset_files"."assetId" = "assets"."id"
) as agg
) as "files"
from
"assets"
where
"assets"."id" = $1
-- AssetJobRepository.getForStorageTemplateJob
select
"assets"."id",

View File

@ -77,6 +77,16 @@ export class AssetJobRepository {
.stream();
}
@GenerateSql({ params: [DummyValue.UUID] })
getForMigrationJob(id: string) {
return this.db
.selectFrom('assets')
.select(['assets.id', 'assets.ownerId', 'assets.encodedVideoPath'])
.select(withFiles)
.where('assets.id', '=', id)
.executeTakeFirst();
}
private storageTemplateAssetQuery() {
return this.db
.selectFrom('assets')

View File

@ -191,13 +191,14 @@ describe(MediaService.name, () => {
describe('handleAssetMigration', () => {
it('should fail if asset does not exist', async () => {
mocks.assetJob.getForMigrationJob.mockResolvedValue(void 0);
await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
expect(mocks.move.getByEntity).not.toHaveBeenCalled();
});
it('should move asset files', async () => {
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
mocks.assetJob.getForMigrationJob.mockResolvedValue(assetStub.image);
mocks.move.create.mockResolvedValue({
entityId: assetStub.image.id,
id: 'move-id',

View File

@ -121,7 +121,7 @@ export class MediaService extends BaseService {
@OnJob({ name: JobName.MIGRATE_ASSET, queue: QueueName.MIGRATION })
async handleAssetMigration({ id }: JobOf<JobName.MIGRATE_ASSET>): Promise<JobStatus> {
const { image } = await this.getConfig({ withCache: true });
const [asset] = await this.assetRepository.getByIds([id], { files: true });
const asset = await this.assetJobRepository.getForMigrationJob(id);
if (!asset) {
return JobStatus.FAILED;
}