diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index fa4c3f4ae..04c84d931 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -21,6 +21,8 @@ export type AuthDto = { export const isMetadataHidden = (auth?: AuthDto) => !!auth?.sharedLink && !auth.sharedLink.showExif; +export const isEditedOnly = (auth: AuthDto, edited = false) => !!auth.sharedLink || edited; + const LoginCredentialSchema = z .object({ email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }), diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 9ee927039..5328d98f4 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -18,7 +18,7 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto.js'; import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto.js'; -import { AuthDto, isMetadataHidden } from 'src/dtos/auth.dto.js'; +import { AuthDto, isEditedOnly, isMetadataHidden } from 'src/dtos/auth.dto.js'; import { AssetFileType, AssetVisibility, @@ -231,13 +231,9 @@ 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, + isEditedOnly(auth, dto.edited), ); const path = editedPath ?? originalPath!; @@ -261,18 +257,11 @@ export class AssetMediaService extends BaseService { throw new BadRequestException('May not request original file'); } - if (auth.sharedLink) { - dto.edited = true; - } - + const edited = isEditedOnly(auth, dto.edited); const size = (dto.size ?? AssetMediaSize.THUMBNAIL) as unknown as AssetFileType; - const { originalPath, originalFileName, path } = await this.assetRepository.getForThumbnail( - id, - size, - dto.edited ?? false, - ); + const { originalPath, originalFileName, path } = await this.assetRepository.getForThumbnail(id, size, edited); - if (size === AssetFileType.FullSize && mimeTypes.isWebSupportedImage(originalPath) && !dto.edited) { + if (size === AssetFileType.FullSize && mimeTypes.isWebSupportedImage(originalPath) && !edited) { // use original file for web supported images return { targetSize: 'original' }; } diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 5c30bb169..700e539b7 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -50,6 +50,49 @@ describe(DownloadService.name, () => { expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset.originalPath, asset.originalFileName); }); + it('should return the original file when the unedited version is requested', async () => { + const archiveMock = { + addFile: vitest.fn(), + finalize: vitest.fn(), + stream: new Readable(), + }; + const asset = AssetFactory.create(); + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForOriginals.mockResolvedValue([asset]); + mocks.storage.createZipStream.mockReturnValue(archiveMock); + + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id], edited: false })).resolves.toEqual({ + stream: archiveMock.stream, + }); + + expect(mocks.asset.getForOriginals).toHaveBeenCalledWith([asset.id], false); + expect(archiveMock.addFile).toHaveBeenCalledWith(asset.originalPath, asset.originalFileName); + }); + + it('should not return the unedited version when requested using a shared link', async () => { + const archiveMock = { + addFile: vitest.fn(), + finalize: vitest.fn(), + stream: new Readable(), + }; + const asset = AssetFactory.create(); + const editedPath = '/data/edited.jpg'; + + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForOriginals.mockResolvedValue([{ ...asset, editedPath }]); + mocks.storage.createZipStream.mockReturnValue(archiveMock); + + await expect( + sut.downloadArchive(authStub.adminSharedLink, { assetIds: [asset.id], edited: false }), + ).resolves.toEqual({ + stream: archiveMock.stream, + }); + + expect(mocks.asset.getForOriginals).toHaveBeenCalledWith([asset.id], true); + expect(archiveMock.addFile).toHaveBeenCalledWith(editedPath, asset.originalFileName); + }); + it('should log a warning if the original path could not be resolved', async () => { const archiveMock = { addFile: vitest.fn(), diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 3cdd48a15..d573e423e 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core.js'; -import { AuthDto } from 'src/dtos/auth.dto.js'; +import { AuthDto, isEditedOnly } from 'src/dtos/auth.dto.js'; import { DownloadArchiveDto, DownloadArchiveInfo, @@ -90,8 +90,9 @@ export class DownloadService extends BaseService { async downloadArchive(auth: AuthDto, dto: DownloadArchiveDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: dto.assetIds }); + const edited = isEditedOnly(auth, dto.edited); const zip = this.storageRepository.createZipStream(); - const assets = await this.assetRepository.getForOriginals(dto.assetIds, dto.edited ?? false); + const assets = await this.assetRepository.getForOriginals(dto.assetIds, edited); const assetMap = new Map(assets.map((asset) => [asset.id, asset])); const paths: Record = {}; @@ -111,7 +112,7 @@ export class DownloadService extends BaseService { filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`; } - let realpath = dto.edited && editedPath ? editedPath : originalPath; + let realpath = edited && editedPath ? editedPath : originalPath; try { realpath = await this.storageRepository.realpath(realpath);