diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index d6dc564458..d1b4e5a72e 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 diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index dc6e9e2573..de994b08cd 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.spec.ts b/server/src/services/asset.service.spec.ts index 8c646e45b9..878721e0a7 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 0a9aa7f355..32c6526394 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -363,11 +363,11 @@ 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); } - 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 e9246c62b1..13bc1ca9a9 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,51 @@ 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(); + 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] }, + }); + }); + }); });