mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 08:49:01 +03:00
fix: shared-link-mapper (#24794)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString } from 'class-validator';
|
||||
import _ from 'lodash';
|
||||
import { SharedLink } from 'src/database';
|
||||
import { HistoryBuilder, Property } from 'src/decorators';
|
||||
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
|
||||
@@ -118,10 +117,10 @@ export class SharedLinkResponseDto {
|
||||
slug!: string | null;
|
||||
}
|
||||
|
||||
export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto {
|
||||
const assets = sharedLink.assets || [];
|
||||
|
||||
return {
|
||||
const response = {
|
||||
id: sharedLink.id,
|
||||
description: sharedLink.description,
|
||||
password: sharedLink.password,
|
||||
@@ -130,35 +129,19 @@ export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: linkAssets.map((asset) => mapAsset(asset)),
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
slug: sharedLink.slug,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapSharedLinkWithoutMetadata(sharedLink: SharedLink): SharedLinkResponseDto {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||
|
||||
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||
|
||||
return {
|
||||
id: sharedLink.id,
|
||||
description: sharedLink.description,
|
||||
password: sharedLink.password,
|
||||
userId: sharedLink.userId,
|
||||
key: sharedLink.key.toString('base64url'),
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })),
|
||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })),
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
slug: sharedLink.slug,
|
||||
};
|
||||
|
||||
// unless we select sharedLink.album.sharedLinks this will be wrong
|
||||
if (response.album) {
|
||||
response.album.hasSharedLink = true;
|
||||
response.album.shared = true;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ describe(SharedLinkService.name, () => {
|
||||
},
|
||||
});
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
||||
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata);
|
||||
const response = await sut.getMine(authDto, {});
|
||||
expect(response.assets[0]).toMatchObject({ hasMetadata: false });
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
mapSharedLink,
|
||||
mapSharedLinkWithoutMetadata,
|
||||
SharedLinkCreateDto,
|
||||
SharedLinkEditDto,
|
||||
SharedLinkPasswordDto,
|
||||
@@ -22,7 +21,7 @@ export class SharedLinkService extends BaseService {
|
||||
async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
|
||||
return this.sharedLinkRepository
|
||||
.getAll({ userId: auth.user.id, id, albumId })
|
||||
.then((links) => links.map((link) => mapSharedLink(link)));
|
||||
.then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false })));
|
||||
}
|
||||
|
||||
async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
|
||||
@@ -31,7 +30,7 @@ export class SharedLinkService extends BaseService {
|
||||
}
|
||||
|
||||
const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);
|
||||
const response = this.mapToSharedLink(sharedLink, { withExif: sharedLink.showExif });
|
||||
const response = mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif });
|
||||
if (sharedLink.password) {
|
||||
response.token = this.validateAndRefreshToken(sharedLink, dto);
|
||||
}
|
||||
@@ -41,7 +40,7 @@ export class SharedLinkService extends BaseService {
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<SharedLinkResponseDto> {
|
||||
const sharedLink = await this.findOrFail(auth.user.id, id);
|
||||
return this.mapToSharedLink(sharedLink, { withExif: true });
|
||||
return mapSharedLink(sharedLink, { stripAssetMetadata: false });
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> {
|
||||
@@ -81,7 +80,7 @@ export class SharedLinkService extends BaseService {
|
||||
slug: dto.slug || null,
|
||||
});
|
||||
|
||||
return this.mapToSharedLink(sharedLink, { withExif: true });
|
||||
return mapSharedLink(sharedLink, { stripAssetMetadata: false });
|
||||
} catch (error) {
|
||||
this.handleError(error);
|
||||
}
|
||||
@@ -108,7 +107,7 @@ export class SharedLinkService extends BaseService {
|
||||
showExif: dto.showMetadata,
|
||||
slug: dto.slug || null,
|
||||
});
|
||||
return this.mapToSharedLink(sharedLink, { withExif: true });
|
||||
return mapSharedLink(sharedLink, { stripAssetMetadata: false });
|
||||
} catch (error) {
|
||||
this.handleError(error);
|
||||
}
|
||||
@@ -214,10 +213,6 @@ export class SharedLinkService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
private mapToSharedLink(sharedLink: SharedLink, { withExif }: { withExif: boolean }) {
|
||||
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink);
|
||||
}
|
||||
|
||||
private validateAndRefreshToken(sharedLink: SharedLink, dto: SharedLinkPasswordDto): string {
|
||||
const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`);
|
||||
const sharedLinkTokens = dto.token?.split(',') || [];
|
||||
|
||||
267
server/test/fixtures/shared-link.stub.ts
vendored
267
server/test/fixtures/shared-link.stub.ts
vendored
@@ -1,10 +1,7 @@
|
||||
import { UserAdmin } from 'src/database';
|
||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto, MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { ExifResponseDto } from 'src/dtos/exif.dto';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
|
||||
import { mapUser } from 'src/dtos/user.dto';
|
||||
import { AssetOrder, AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum';
|
||||
import { AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
@@ -20,89 +17,6 @@ const sharedLinkBytes = Buffer.from(
|
||||
'hex',
|
||||
);
|
||||
|
||||
const assetInfo: ExifResponseDto = {
|
||||
make: 'camera-make',
|
||||
model: 'camera-model',
|
||||
exifImageWidth: 500,
|
||||
exifImageHeight: 500,
|
||||
fileSizeInByte: 100,
|
||||
orientation: 'orientation',
|
||||
dateTimeOriginal: today,
|
||||
modifyDate: today,
|
||||
timeZone: 'America/Los_Angeles',
|
||||
lensModel: 'fancy',
|
||||
fNumber: 100,
|
||||
focalLength: 100,
|
||||
iso: 100,
|
||||
exposureTime: '1/16',
|
||||
latitude: 100,
|
||||
longitude: 100,
|
||||
city: 'city',
|
||||
state: 'state',
|
||||
country: 'country',
|
||||
description: 'description',
|
||||
projectionType: null,
|
||||
};
|
||||
|
||||
const assetResponse: AssetResponseDto = {
|
||||
id: 'id_1',
|
||||
createdAt: today,
|
||||
deviceAssetId: 'device_asset_id_1',
|
||||
ownerId: 'user_id_1',
|
||||
deviceId: 'device_id_1',
|
||||
type: AssetType.Video,
|
||||
originalMimeType: 'image/jpeg',
|
||||
originalPath: 'fake_path/jpeg',
|
||||
originalFileName: 'asset_1.jpeg',
|
||||
thumbhash: null,
|
||||
fileModifiedAt: today,
|
||||
isOffline: false,
|
||||
fileCreatedAt: today,
|
||||
localDateTime: today,
|
||||
updatedAt: today,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
duration: '0:00:00.00000',
|
||||
exifInfo: assetInfo,
|
||||
livePhotoVideoId: null,
|
||||
tags: [],
|
||||
people: [],
|
||||
checksum: 'ZmlsZSBoYXNo',
|
||||
isTrashed: false,
|
||||
libraryId: 'library-id',
|
||||
hasMetadata: true,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
};
|
||||
|
||||
const assetResponseWithoutMetadata = {
|
||||
id: 'id_1',
|
||||
type: AssetType.Video,
|
||||
originalMimeType: 'image/jpeg',
|
||||
thumbhash: null,
|
||||
localDateTime: today,
|
||||
duration: '0:00:00.00000',
|
||||
livePhotoVideoId: null,
|
||||
hasMetadata: false,
|
||||
} as AssetResponseDto;
|
||||
|
||||
const albumResponse: AlbumResponseDto = {
|
||||
albumName: 'Test Album',
|
||||
description: '',
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: today,
|
||||
updatedAt: today,
|
||||
id: 'album-123',
|
||||
ownerId: 'admin_id',
|
||||
owner: mapUser(userStub.admin),
|
||||
albumUsers: [],
|
||||
shared: false,
|
||||
hasSharedLink: false,
|
||||
assets: [],
|
||||
assetCount: 1,
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.Desc,
|
||||
};
|
||||
|
||||
export const sharedLinkStub = {
|
||||
individual: Object.freeze({
|
||||
id: '123',
|
||||
@@ -161,7 +75,7 @@ export const sharedLinkStub = {
|
||||
id: '123',
|
||||
userId: authStub.admin.user.id,
|
||||
key: sharedLinkBytes,
|
||||
type: SharedLinkType.Album,
|
||||
type: SharedLinkType.Individual,
|
||||
createdAt: today,
|
||||
expiresAt: tomorrow,
|
||||
allowUpload: false,
|
||||
@@ -169,97 +83,80 @@ export const sharedLinkStub = {
|
||||
showExif: false,
|
||||
description: null,
|
||||
password: null,
|
||||
assets: [],
|
||||
slug: null,
|
||||
albumId: 'album-123',
|
||||
album: {
|
||||
id: 'album-123',
|
||||
updateId: '42',
|
||||
ownerId: authStub.admin.user.id,
|
||||
owner: userStub.admin,
|
||||
albumName: 'Test Album',
|
||||
description: '',
|
||||
createdAt: today,
|
||||
updatedAt: today,
|
||||
deletedAt: null,
|
||||
albumThumbnailAsset: null,
|
||||
albumThumbnailAssetId: null,
|
||||
albumUsers: [],
|
||||
sharedLinks: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.Desc,
|
||||
assets: [
|
||||
{
|
||||
id: 'id_1',
|
||||
status: AssetStatus.Active,
|
||||
owner: undefined as unknown as UserAdmin,
|
||||
ownerId: 'user_id_1',
|
||||
deviceAssetId: 'device_asset_id_1',
|
||||
deviceId: 'device_id_1',
|
||||
type: AssetType.Video,
|
||||
originalPath: 'fake_path/jpeg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
fileModifiedAt: today,
|
||||
fileCreatedAt: today,
|
||||
localDateTime: today,
|
||||
createdAt: today,
|
||||
assets: [
|
||||
{
|
||||
id: 'id_1',
|
||||
status: AssetStatus.Active,
|
||||
owner: undefined as unknown as UserAdmin,
|
||||
ownerId: 'user_id_1',
|
||||
deviceAssetId: 'device_asset_id_1',
|
||||
deviceId: 'device_id_1',
|
||||
type: AssetType.Video,
|
||||
originalPath: 'fake_path/jpeg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
fileModifiedAt: today,
|
||||
fileCreatedAt: today,
|
||||
localDateTime: today,
|
||||
createdAt: today,
|
||||
updatedAt: today,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isExternal: false,
|
||||
isOffline: false,
|
||||
files: [],
|
||||
thumbhash: null,
|
||||
encodedVideoPath: '',
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
originalFileName: 'asset_1.jpeg',
|
||||
exifInfo: {
|
||||
projectionType: null,
|
||||
livePhotoCID: null,
|
||||
assetId: 'id_1',
|
||||
description: 'description',
|
||||
exifImageWidth: 500,
|
||||
exifImageHeight: 500,
|
||||
fileSizeInByte: 100,
|
||||
orientation: 'orientation',
|
||||
dateTimeOriginal: today,
|
||||
modifyDate: today,
|
||||
timeZone: 'America/Los_Angeles',
|
||||
latitude: 100,
|
||||
longitude: 100,
|
||||
city: 'city',
|
||||
state: 'state',
|
||||
country: 'country',
|
||||
make: 'camera-make',
|
||||
model: 'camera-model',
|
||||
lensModel: 'fancy',
|
||||
fNumber: 100,
|
||||
focalLength: 100,
|
||||
iso: 100,
|
||||
exposureTime: '1/16',
|
||||
fps: 100,
|
||||
profileDescription: 'sRGB',
|
||||
bitsPerSample: 8,
|
||||
colorspace: 'sRGB',
|
||||
autoStackId: null,
|
||||
rating: 3,
|
||||
updatedAt: today,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isExternal: false,
|
||||
isOffline: false,
|
||||
files: [],
|
||||
thumbhash: null,
|
||||
encodedVideoPath: '',
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
originalFileName: 'asset_1.jpeg',
|
||||
exifInfo: {
|
||||
projectionType: null,
|
||||
livePhotoCID: null,
|
||||
assetId: 'id_1',
|
||||
description: 'description',
|
||||
exifImageWidth: 500,
|
||||
exifImageHeight: 500,
|
||||
fileSizeInByte: 100,
|
||||
orientation: 'orientation',
|
||||
dateTimeOriginal: today,
|
||||
modifyDate: today,
|
||||
timeZone: 'America/Los_Angeles',
|
||||
latitude: 100,
|
||||
longitude: 100,
|
||||
city: 'city',
|
||||
state: 'state',
|
||||
country: 'country',
|
||||
make: 'camera-make',
|
||||
model: 'camera-model',
|
||||
lensModel: 'fancy',
|
||||
fNumber: 100,
|
||||
focalLength: 100,
|
||||
iso: 100,
|
||||
exposureTime: '1/16',
|
||||
fps: 100,
|
||||
profileDescription: 'sRGB',
|
||||
bitsPerSample: 8,
|
||||
colorspace: 'sRGB',
|
||||
autoStackId: null,
|
||||
rating: 3,
|
||||
updatedAt: today,
|
||||
updateId: '42',
|
||||
},
|
||||
sharedLinks: [],
|
||||
faces: [],
|
||||
sidecarPath: null,
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
updateId: '42',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
},
|
||||
],
|
||||
},
|
||||
sharedLinks: [],
|
||||
faces: [],
|
||||
sidecarPath: null,
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
updateId: '42',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
},
|
||||
],
|
||||
albumId: null,
|
||||
album: null,
|
||||
slug: null,
|
||||
}),
|
||||
passwordRequired: Object.freeze({
|
||||
id: '123',
|
||||
@@ -312,20 +209,4 @@ export const sharedLinkResponseStub = {
|
||||
userId: 'admin_id',
|
||||
slug: null,
|
||||
}),
|
||||
readonlyNoMetadata: Object.freeze<SharedLinkResponseDto>({
|
||||
id: '123',
|
||||
userId: 'admin_id',
|
||||
key: sharedLinkBytes.toString('base64url'),
|
||||
type: SharedLinkType.Album,
|
||||
createdAt: today,
|
||||
expiresAt: tomorrow,
|
||||
description: null,
|
||||
password: null,
|
||||
allowUpload: false,
|
||||
allowDownload: false,
|
||||
showMetadata: false,
|
||||
slug: null,
|
||||
album: { ...albumResponse, startDate: assetResponse.localDateTime, endDate: assetResponse.localDateTime },
|
||||
assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }],
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user