fix: Download the edited version when downloading multiple photos (#26259)

* fix: download the edited version when downloading multiple photos

* test: update tests

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
Jorge Montejo
2026-02-18 22:47:45 +01:00
committed by GitHub
parent ea30c9d2ba
commit 1f8359ead4
15 changed files with 215 additions and 35 deletions

View File

@@ -39,7 +39,7 @@ describe(DownloadService.name, () => {
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id, 'unknown-asset']));
mocks.asset.getByIds.mockResolvedValue([asset]);
mocks.asset.getForOriginals.mockResolvedValue([asset]);
mocks.storage.createZipStream.mockReturnValue(archiveMock);
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id, 'unknown-asset'] })).resolves.toEqual({
@@ -62,7 +62,7 @@ describe(DownloadService.name, () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
mocks.storage.realpath.mockRejectedValue(new Error('Could not read file'));
mocks.asset.getByIds.mockResolvedValue([asset1, asset2]);
mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]);
mocks.storage.createZipStream.mockReturnValue(archiveMock);
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
@@ -86,7 +86,7 @@ describe(DownloadService.name, () => {
const asset2 = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
mocks.asset.getByIds.mockResolvedValue([asset1, asset2]);
mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]);
mocks.storage.createZipStream.mockReturnValue(archiveMock);
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
@@ -108,7 +108,7 @@ describe(DownloadService.name, () => {
const asset2 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' });
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
mocks.asset.getByIds.mockResolvedValue([asset1, asset2]);
mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]);
mocks.storage.createZipStream.mockReturnValue(archiveMock);
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
@@ -130,7 +130,7 @@ describe(DownloadService.name, () => {
const asset2 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' });
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
mocks.asset.getByIds.mockResolvedValue([asset2, asset1]);
mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]);
mocks.storage.createZipStream.mockReturnValue(archiveMock);
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
@@ -151,7 +151,7 @@ describe(DownloadService.name, () => {
const asset = AssetFactory.create({ originalPath: '/path/to/symlink.jpg' });
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getByIds.mockResolvedValue([asset]);
mocks.asset.getForOriginals.mockResolvedValue([asset]);
mocks.storage.realpath.mockResolvedValue('/path/to/realpath.jpg');
mocks.storage.createZipStream.mockReturnValue(archiveMock);

View File

@@ -1,9 +1,8 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { parse } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
import { DownloadArchiveDto, DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
import { Permission } from 'src/enum';
import { ImmichReadStream } from 'src/repositories/storage.repository';
import { BaseService } from 'src/services/base.service';
@@ -80,11 +79,11 @@ export class DownloadService extends BaseService {
return { totalSize, archives };
}
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
async downloadArchive(auth: AuthDto, dto: DownloadArchiveDto): Promise<ImmichReadStream> {
await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: dto.assetIds });
const zip = this.storageRepository.createZipStream();
const assets = await this.assetRepository.getByIds(dto.assetIds);
const assets = await this.assetRepository.getForOriginals(dto.assetIds, dto.edited ?? false);
const assetMap = new Map(assets.map((asset) => [asset.id, asset]));
const paths: Record<string, number> = {};
@@ -94,7 +93,7 @@ export class DownloadService extends BaseService {
continue;
}
const { originalPath, originalFileName } = asset;
const { originalPath, editedPath, originalFileName } = asset;
let filename = originalFileName;
const count = paths[filename] || 0;
@@ -104,9 +103,10 @@ export class DownloadService extends BaseService {
filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`;
}
let realpath = originalPath;
let realpath = dto.edited && editedPath ? editedPath : originalPath;
try {
realpath = await this.storageRepository.realpath(originalPath);
realpath = await this.storageRepository.realpath(realpath);
} catch {
this.logger.warn('Unable to resolve realpath', { originalPath });
}