fix(server): only serve edited assets to shared links in archive downloads (#32562)
(cherry picked from commit cd47ee30676e8575a5048d12cde2ee86fc02b965)
This commit is contained in:
@@ -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' }),
|
||||
|
||||
@@ -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<ImmichFileResponse> {
|
||||
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' };
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<ImmichReadStream> {
|
||||
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<string, number> = {};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user