From f9dc616bd6697c60e12396986fe721c4be4b4589 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:31:10 -0500 Subject: [PATCH 1/5] do not delete isOffline assets --- server/src/repositories/asset-job.repository.ts | 1 + server/src/services/asset.service.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index dc6e9e2573dbb..de994b08cd71f 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -232,6 +232,7 @@ export class AssetJobRepository { 'asset.livePhotoVideoId', 'asset.encodedVideoPath', 'asset.originalPath', + 'asset.isOffline', ]) .$call(withExif) .select(withFacesAndPeople) diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 0a9aa7f35533b..45015b23d2658 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -363,7 +363,7 @@ export class AssetService extends BaseService { const { fullsizeFile, previewFile, thumbnailFile, sidecarFile } = getAssetFiles(asset.files ?? []); const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath]; - if (deleteOnDisk) { + if (deleteOnDisk && !asset.isOffline) { files.push(sidecarFile?.path, asset.originalPath); } From 21eaa74d8dd79622930ca2fdb69cd4dc63dd57a9 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:38:08 -0500 Subject: [PATCH 2/5] update sql --- server/src/queries/asset.job.repository.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index d6dc5644585d7..d1b4e5a72e6d6 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -369,6 +369,7 @@ select "asset"."livePhotoVideoId", "asset"."encodedVideoPath", "asset"."originalPath", + "asset"."isOffline", to_json("asset_exif") as "exifInfo", ( select From f6d8575ef9f552818e9dc89f29c9782b0dfe51e2 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:50:32 -0500 Subject: [PATCH 3/5] add medium test --- server/src/services/asset.service.spec.ts | 6 --- server/src/services/asset.service.ts | 2 +- .../specs/services/asset.service.spec.ts | 39 ++++++++++++++++++- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 8c646e45b9752..878721e0a7098 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -585,8 +585,6 @@ describe(AssetService.name, () => { '/uploads/user-id/webp/path.ext', '/uploads/user-id/thumbs/path.jpg', '/uploads/user-id/fullsize/path.webp', - assetWithFace.encodedVideoPath, // this value is null - undefined, // no sidecar path assetWithFace.originalPath, ], }, @@ -648,8 +646,6 @@ describe(AssetService.name, () => { '/uploads/user-id/webp/path.ext', '/uploads/user-id/thumbs/path.jpg', '/uploads/user-id/fullsize/path.webp', - undefined, - undefined, 'fake_path/asset_1.jpeg', ], }, @@ -676,8 +672,6 @@ describe(AssetService.name, () => { '/uploads/user-id/webp/path.ext', '/uploads/user-id/thumbs/path.jpg', '/uploads/user-id/fullsize/path.webp', - undefined, - undefined, 'fake_path/asset_1.jpeg', ], }, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 45015b23d2658..32c65263948e3 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -367,7 +367,7 @@ export class AssetService extends BaseService { files.push(sidecarFile?.path, asset.originalPath); } - await this.jobRepository.queue({ name: JobName.FileDelete, data: { files } }); + await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: files.filter(Boolean) } }); return JobStatus.Success; } diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index e9246c62b1647..5fd127a9ebe50 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -2,13 +2,16 @@ import { Kysely } from 'kysely'; import { AssetFileType, JobName, SharedLinkType } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; +import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; +import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { SharedLinkAssetRepository } from 'src/repositories/shared-link-asset.repository'; import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; import { StackRepository } from 'src/repositories/stack.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; +import { UserRepository } from 'src/repositories/user.repository'; import { DB } from 'src/schema'; import { AssetService } from 'src/services/asset.service'; import { newMediumService } from 'test/medium.factory'; @@ -20,8 +23,16 @@ let defaultDatabase: Kysely; const setup = (db?: Kysely) => { return newMediumService(AssetService, { database: db || defaultDatabase, - real: [AssetRepository, AlbumRepository, AccessRepository, SharedLinkAssetRepository, StackRepository], - mock: [LoggingRepository, JobRepository, StorageRepository], + real: [ + AssetRepository, + AssetJobRepository, + AlbumRepository, + AccessRepository, + SharedLinkAssetRepository, + StackRepository, + UserRepository, + ], + mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository], }); }; @@ -210,4 +221,28 @@ describe(AssetService.name, () => { }); }); }); + + describe('delete', () => { + it('should not delete offline assets', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, isOffline: true }); + const thumbnailPath = '/path/to/thumbnail.jpg'; + const previewPath = '/path/to/preview.jpg'; + await Promise.all([ + ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: thumbnailPath }), + ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: previewPath }), + ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: `/path/to/sidecar.xmp` }), + ]); + + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); + + expect(ctx.getMock(JobRepository).queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { files: [thumbnailPath, previewPath] }, + }); + }); + }); }); From e7f68c04e0320eaf3ebee4e3567c8da62a76fba8 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:52:16 -0500 Subject: [PATCH 4/5] add normal delete test --- .../specs/services/asset.service.spec.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index 5fd127a9ebe50..a1c9f545f4de6 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -223,6 +223,29 @@ describe(AssetService.name, () => { }); describe('delete', () => { + it('should delete asset', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const thumbnailPath = '/path/to/thumbnail.jpg'; + const previewPath = '/path/to/preview.jpg'; + const sidecarPath = '/path/to/sidecar.xmp'; + await Promise.all([ + ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: thumbnailPath }), + ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: previewPath }), + ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: sidecarPath }), + ]); + + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); + + expect(ctx.getMock(JobRepository).queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { files: [thumbnailPath, previewPath, sidecarPath, asset.originalPath ] }, + }); + }); + it('should not delete offline assets', async () => { const { sut, ctx } = setup(); ctx.getMock(EventRepository).emit.mockResolvedValue(); From 1b80223a97bfbe33cacd1e546ff927bad46fcdb3 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:58:31 -0500 Subject: [PATCH 5/5] formatting --- server/test/medium/specs/services/asset.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index a1c9f545f4de6..13bc1ca9a9369 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -242,7 +242,7 @@ describe(AssetService.name, () => { expect(ctx.getMock(JobRepository).queue).toHaveBeenCalledWith({ name: JobName.FileDelete, - data: { files: [thumbnailPath, previewPath, sidecarPath, asset.originalPath ] }, + data: { files: [thumbnailPath, previewPath, sidecarPath, asset.originalPath] }, }); });