From 42b354c302dfb3609e407c74c4428a26d8256bae Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Mon, 26 Jan 2026 16:42:22 -0600 Subject: [PATCH] fix: always serve edited version if using shared link. (#25536) * fix: always serve edited version if using shared link. * chore: test * chore: rename tests --- .../src/services/asset-media.service.spec.ts | 51 +++++++++++++++++++ server/src/services/asset-media.service.ts | 8 +++ 2 files changed, 59 insertions(+) diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 049e7ec6ac..0bcb87e2f4 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -572,6 +572,35 @@ describe(AssetMediaService.name, () => { ); }); + it('should not return the unedited version if requested using a shared link', async () => { + const editedAsset = { + ...assetStub.withCropEdit, + files: [ + ...assetStub.withCropEdit.files, + { + id: 'edited-file', + type: AssetFileType.FullSize, + path: '/uploads/user-id/fullsize/edited.jpg', + isEdited: true, + }, + ], + }; + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); + mocks.asset.getForOriginal.mockResolvedValue({ + ...editedAsset, + editedPath: '/uploads/user-id/fullsize/edited.jpg', + }); + + await expect(sut.downloadOriginal(authStub.adminSharedLink, 'asset-id', { edited: false })).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/fullsize/edited.jpg', + fileName: 'asset-id.jpg', + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); + }); + it('should download original file when edited=false', async () => { const editedAsset = { ...assetStub.withCropEdit, @@ -711,6 +740,28 @@ describe(AssetMediaService.name, () => { ); expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false); }); + + it('should not return the unedited version if requested using a shared link', async () => { + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ + ...assetStub.image, + path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', + }); + await expect( + sut.viewThumbnail(authStub.adminSharedLink, assetStub.image.id, { + size: AssetMediaSize.THUMBNAIL, + edited: true, + }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: 'asset-id_thumbnail.jpg', + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, true); + }); }); describe('playbackVideo', () => { diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index f008523029..020bda4df7 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -196,6 +196,10 @@ export class AssetMediaService extends BaseService { async downloadOriginal(auth: AuthDto, id: string, dto: AssetDownloadOriginalDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: [id] }); + if (auth.sharedLink) { + dto.edited = true; + } + const { originalPath, originalFileName, editedPath } = await this.assetRepository.getForOriginal( id, dto.edited ?? false, @@ -222,6 +226,10 @@ export class AssetMediaService extends BaseService { throw new BadRequestException('May not request original file'); } + if (auth.sharedLink) { + dto.edited = true; + } + const size = (dto.size ?? AssetMediaSize.THUMBNAIL) as unknown as AssetFileType; const { originalPath, originalFileName, path } = await this.assetRepository.getForThumbnail( id,