From d14d0a9b9b784de63fa2e79121d128edfe493e03 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Mon, 23 Feb 2026 16:33:52 -0500 Subject: [PATCH] feat: add isTransparent to db (#26413) --- server/src/database.ts | 1 + server/src/queries/asset.job.repository.sql | 3 +- server/src/repositories/asset.repository.ts | 11 ++++- .../1771639515206-AddIsTransparentColumn.ts | 9 ++++ server/src/schema/tables/asset-file.table.ts | 3 ++ server/src/services/media.service.spec.ts | 44 +++++++++++++++++++ server/src/services/media.service.ts | 24 ++++++++-- server/test/factories/asset-file.factory.ts | 1 + 8 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts diff --git a/server/src/database.ts b/server/src/database.ts index dd979fdea6..5d29cbb043 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -352,6 +352,7 @@ export const columns = { 'asset_file.type', 'asset_file.isEdited', 'asset_file.isProgressive', + 'asset_file.isTransparent', ], authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], authApiKey: ['api_key.id', 'api_key.permissions'], diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index d990e0a304..54b3c92dd4 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -216,7 +216,8 @@ select "asset_file"."path", "asset_file"."type", "asset_file"."isEdited", - "asset_file"."isProgressive" + "asset_file"."isProgressive", + "asset_file"."isTransparent" from "asset_file" where diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index e424d4e0b8..b58d852707 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -903,7 +903,10 @@ export class AssetRepository { } async upsertFile( - file: Pick, 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive'>, + file: Pick< + Insertable, + 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive' | 'isTransparent' + >, ): Promise { await this.db .insertInto('asset_file') @@ -917,7 +920,10 @@ export class AssetRepository { } async upsertFiles( - files: Pick, 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive'>[], + files: Pick< + Insertable, + 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive' | 'isTransparent' + >[], ): Promise { if (files.length === 0) { return; @@ -930,6 +936,7 @@ export class AssetRepository { oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({ path: eb.ref('excluded.path'), isProgressive: eb.ref('excluded.isProgressive'), + isTransparent: eb.ref('excluded.isTransparent'), })), ) .execute(); diff --git a/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts b/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts new file mode 100644 index 0000000000..a19d102edb --- /dev/null +++ b/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" ADD "isTransparent" boolean NOT NULL DEFAULT false;`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" DROP COLUMN "isTransparent";`.execute(db); +} diff --git a/server/src/schema/tables/asset-file.table.ts b/server/src/schema/tables/asset-file.table.ts index 7fdde5fed1..6285e4d653 100644 --- a/server/src/schema/tables/asset-file.table.ts +++ b/server/src/schema/tables/asset-file.table.ts @@ -43,4 +43,7 @@ export class AssetFileTable { @Column({ type: 'boolean', default: false }) isProgressive!: Generated; + + @Column({ type: 'boolean', default: false }) + isTransparent!: Generated; } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 368ece625c..5317989739 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -468,6 +468,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -475,6 +476,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, thumbhash: thumbhashBuffer }); @@ -509,6 +511,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -516,6 +519,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, ]); }); @@ -549,6 +553,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -556,6 +561,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, ]); }); @@ -771,10 +777,12 @@ describe(MediaService.name, () => { expect.objectContaining({ type: AssetFileType.Preview, isProgressive: true, + isTransparent: false, }), expect.objectContaining({ type: AssetFileType.Thumbnail, isProgressive: false, + isTransparent: false, }), ]); }); @@ -808,10 +816,12 @@ describe(MediaService.name, () => { expect.objectContaining({ type: AssetFileType.Preview, isProgressive: false, + isTransparent: false, }), expect.objectContaining({ type: AssetFileType.Thumbnail, isProgressive: true, + isTransparent: false, }), ]); }); @@ -830,10 +840,12 @@ describe(MediaService.name, () => { expect.objectContaining({ type: AssetFileType.Preview, isProgressive: false, + isTransparent: false, }), expect.objectContaining({ type: AssetFileType.Thumbnail, isProgressive: false, + isTransparent: false, }), ]); }); @@ -3583,6 +3595,7 @@ describe(MediaService.name, () => { path: '/new/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -3590,6 +3603,7 @@ describe(MediaService.name, () => { path: '/new/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); @@ -3600,6 +3614,7 @@ describe(MediaService.name, () => { type: AssetFileType.Preview, isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: 'asset-id', @@ -3607,6 +3622,7 @@ describe(MediaService.name, () => { type: AssetFileType.Thumbnail, isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); @@ -3624,6 +3640,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3632,6 +3649,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3643,6 +3661,7 @@ describe(MediaService.name, () => { path: '/new/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -3650,6 +3669,7 @@ describe(MediaService.name, () => { path: '/new/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); @@ -3660,6 +3680,7 @@ describe(MediaService.name, () => { type: AssetFileType.Preview, isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: 'asset-id', @@ -3667,6 +3688,7 @@ describe(MediaService.name, () => { type: AssetFileType.Thumbnail, isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); @@ -3687,6 +3709,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3695,6 +3718,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3710,6 +3734,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3718,6 +3743,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -3737,6 +3763,7 @@ describe(MediaService.name, () => { path: '/same/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3745,6 +3772,7 @@ describe(MediaService.name, () => { path: '/same/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3756,6 +3784,7 @@ describe(MediaService.name, () => { path: '/same/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -3763,6 +3792,7 @@ describe(MediaService.name, () => { path: '/same/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); @@ -3782,6 +3812,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3790,6 +3821,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3801,6 +3833,7 @@ describe(MediaService.name, () => { path: '/new/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, // replace { assetId: asset.id, @@ -3808,6 +3841,7 @@ describe(MediaService.name, () => { path: '/new/fullsize.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, // new ]); @@ -3818,6 +3852,7 @@ describe(MediaService.name, () => { type: AssetFileType.Preview, isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: 'asset-id', @@ -3825,6 +3860,7 @@ describe(MediaService.name, () => { type: AssetFileType.FullSize, isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([ @@ -3835,6 +3871,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -3867,6 +3904,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3882,6 +3920,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -3901,6 +3940,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3909,6 +3949,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3920,6 +3961,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: true, + isTransparent: false, }, { assetId: asset.id, @@ -3927,6 +3969,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); @@ -3937,6 +3980,7 @@ describe(MediaService.name, () => { type: AssetFileType.Preview, isEdited: false, isProgressive: true, + isTransparent: false, }, ]); expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 153083142d..3555d7d108 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -52,6 +52,7 @@ interface UpsertFileOptions { path: string; isEdited: boolean; isProgressive: boolean; + isTransparent: boolean; } type ThumbnailAsset = NonNullable>>; @@ -321,12 +322,14 @@ export class MediaService extends BaseService { format: previewFormat, isEdited: useEdits, isProgressive: !!image.preview.progressive && previewFormat !== ImageFormat.Webp, + isTransparent, }); const thumbnailFile = this.getImageFile(asset, { fileType: AssetFileType.Thumbnail, format: thumbnailFormat, isEdited: useEdits, isProgressive: !!image.thumbnail.progressive && thumbnailFormat !== ImageFormat.Webp, + isTransparent, }); this.storageCore.ensureFolders(previewFile.path); @@ -350,6 +353,7 @@ export class MediaService extends BaseService { format: fullsizeFormat, isEdited: useEdits, isProgressive: !!image.fullsize.progressive && fullsizeFormat !== ImageFormat.Webp, + isTransparent, }); const fullsizeOptions = { ...baseOptions, @@ -364,6 +368,7 @@ export class MediaService extends BaseService { format: extracted.format, isEdited: false, isProgressive: !!image.fullsize.progressive && image.fullsize.format !== ImageFormat.Webp, + isTransparent, }); this.storageCore.ensureFolders(fullsizeFile.path); @@ -510,12 +515,14 @@ export class MediaService extends BaseService { format: image.preview.format, isEdited: false, isProgressive: false, + isTransparent: false, }); const thumbnailFile = this.getImageFile(asset, { fileType: AssetFileType.Thumbnail, format: image.thumbnail.format, isEdited: false, isProgressive: false, + isTransparent: false, }); this.storageCore.ensureFolders(previewFile.path); @@ -802,7 +809,10 @@ export class MediaService extends BaseService { } } - private async syncFiles(oldFiles: (AssetFile & { isProgressive: boolean })[], newFiles: UpsertFileOptions[]) { + private async syncFiles( + oldFiles: (AssetFile & { isProgressive: boolean; isTransparent: boolean })[], + newFiles: UpsertFileOptions[], + ) { const toUpsert: UpsertFileOptions[] = []; const pathsToDelete: string[] = []; const toDelete = new Set(oldFiles); @@ -814,7 +824,11 @@ export class MediaService extends BaseService { } // upsert new file path - if (existingFile?.path !== newFile.path || existingFile.isProgressive !== newFile.isProgressive) { + if ( + existingFile?.path !== newFile.path || + existingFile.isProgressive !== newFile.isProgressive || + existingFile.isTransparent !== newFile.isTransparent + ) { toUpsert.push(newFile); // delete old file from disk @@ -882,7 +896,10 @@ export class MediaService extends BaseService { } } - private getImageFile(asset: ThumbnailPathEntity, options: ImagePathOptions & { isProgressive: boolean }) { + private getImageFile( + asset: ThumbnailPathEntity, + options: ImagePathOptions & { isProgressive: boolean; isTransparent: boolean }, + ) { const path = StorageCore.getImagePath(asset, options); return { assetId: asset.id, @@ -890,6 +907,7 @@ export class MediaService extends BaseService { path, isEdited: options.isEdited, isProgressive: options.isProgressive, + isTransparent: options.isTransparent, }; } } diff --git a/server/test/factories/asset-file.factory.ts b/server/test/factories/asset-file.factory.ts index 109cd5adc4..511ab45bb7 100644 --- a/server/test/factories/asset-file.factory.ts +++ b/server/test/factories/asset-file.factory.ts @@ -26,6 +26,7 @@ export class AssetFileFactory { path: `/data/12/34/thumbs/${id.slice(0, 2)}/${id.slice(2, 4)}/${id}${isEdited ? '_edited' : ''}.jpg`, updateId: newUuidV7(), isProgressive: false, + isTransparent: false, isEdited, ...dto, });