mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01:00
fix(server,web): correctly remove metadata from shared links (#4464)
* wip: strip metadata * fix: authenticate time buckets * hide detail panel * fix tests * fix lint * add e2e tests * chore: open api * fix web compilation error * feat: test with asset with gps position * fix: only import fs.promises.cp * fix: cleanup mapasset * fix: format --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
4a9f58bf9b
commit
dadcf49eca
39 changed files with 283 additions and 113 deletions
14
cli/src/api/open-api/api.ts
generated
14
cli/src/api/open-api/api.ts
generated
|
@ -640,6 +640,12 @@ export interface AssetResponseDto {
|
||||||
* @memberof AssetResponseDto
|
* @memberof AssetResponseDto
|
||||||
*/
|
*/
|
||||||
'fileModifiedAt': string;
|
'fileModifiedAt': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetResponseDto
|
||||||
|
*/
|
||||||
|
'hasMetadata': boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -749,7 +755,7 @@ export interface AssetResponseDto {
|
||||||
*/
|
*/
|
||||||
'tags'?: Array<TagResponseDto>;
|
'tags'?: Array<TagResponseDto>;
|
||||||
/**
|
/**
|
||||||
* base64 encoded thumbhash
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AssetResponseDto
|
* @memberof AssetResponseDto
|
||||||
*/
|
*/
|
||||||
|
@ -2882,7 +2888,7 @@ export interface SharedLinkCreateDto {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof SharedLinkCreateDto
|
* @memberof SharedLinkCreateDto
|
||||||
*/
|
*/
|
||||||
'showExif'?: boolean;
|
'showMetadata'?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SharedLinkType}
|
* @type {SharedLinkType}
|
||||||
|
@ -2927,7 +2933,7 @@ export interface SharedLinkEditDto {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof SharedLinkEditDto
|
* @memberof SharedLinkEditDto
|
||||||
*/
|
*/
|
||||||
'showExif'?: boolean;
|
'showMetadata'?: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -2994,7 +3000,7 @@ export interface SharedLinkResponseDto {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof SharedLinkResponseDto
|
* @memberof SharedLinkResponseDto
|
||||||
*/
|
*/
|
||||||
'showExif': boolean;
|
'showMetadata': boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SharedLinkType}
|
* @type {SharedLinkType}
|
||||||
|
|
BIN
mobile/openapi/doc/AssetResponseDto.md
generated
BIN
mobile/openapi/doc/AssetResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SharedLinkCreateDto.md
generated
BIN
mobile/openapi/doc/SharedLinkCreateDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SharedLinkEditDto.md
generated
BIN
mobile/openapi/doc/SharedLinkEditDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SharedLinkResponseDto.md
generated
BIN
mobile/openapi/doc/SharedLinkResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_response_dto.dart
generated
BIN
mobile/openapi/lib/model/asset_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/shared_link_create_dto.dart
generated
BIN
mobile/openapi/lib/model/shared_link_create_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/shared_link_edit_dto.dart
generated
BIN
mobile/openapi/lib/model/shared_link_edit_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/shared_link_response_dto.dart
generated
BIN
mobile/openapi/lib/model/shared_link_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_response_dto_test.dart
generated
BIN
mobile/openapi/test/asset_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/shared_link_create_dto_test.dart
generated
BIN
mobile/openapi/test/shared_link_create_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/shared_link_edit_dto_test.dart
generated
BIN
mobile/openapi/test/shared_link_edit_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/shared_link_response_dto_test.dart
generated
BIN
mobile/openapi/test/shared_link_response_dto_test.dart
generated
Binary file not shown.
|
@ -5770,6 +5770,9 @@
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hasMetadata": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -5833,7 +5836,6 @@
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"thumbhash": {
|
"thumbhash": {
|
||||||
"description": "base64 encoded thumbhash",
|
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -5847,7 +5849,6 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"id",
|
|
||||||
"deviceAssetId",
|
"deviceAssetId",
|
||||||
"deviceId",
|
"deviceId",
|
||||||
"ownerId",
|
"ownerId",
|
||||||
|
@ -5855,19 +5856,21 @@
|
||||||
"originalPath",
|
"originalPath",
|
||||||
"originalFileName",
|
"originalFileName",
|
||||||
"resized",
|
"resized",
|
||||||
"thumbhash",
|
|
||||||
"fileCreatedAt",
|
"fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"fileModifiedAt",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
"isFavorite",
|
"isFavorite",
|
||||||
"isArchived",
|
"isArchived",
|
||||||
"isTrashed",
|
"isTrashed",
|
||||||
"localDateTime",
|
|
||||||
"isOffline",
|
"isOffline",
|
||||||
"isExternal",
|
"isExternal",
|
||||||
"isReadOnly",
|
"isReadOnly",
|
||||||
|
"checksum",
|
||||||
|
"id",
|
||||||
|
"thumbhash",
|
||||||
|
"localDateTime",
|
||||||
"duration",
|
"duration",
|
||||||
"checksum"
|
"hasMetadata"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
@ -7599,7 +7602,7 @@
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"showExif": {
|
"showMetadata": {
|
||||||
"default": true,
|
"default": true,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -7628,7 +7631,7 @@
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"showExif": {
|
"showMetadata": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -7670,7 +7673,7 @@
|
||||||
"key": {
|
"key": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"showExif": {
|
"showMetadata": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
|
@ -7691,7 +7694,7 @@
|
||||||
"assets",
|
"assets",
|
||||||
"allowUpload",
|
"allowUpload",
|
||||||
"allowDownload",
|
"allowDownload",
|
||||||
"showExif"
|
"showMetadata"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,6 +47,7 @@ import {
|
||||||
BulkIdsDto,
|
BulkIdsDto,
|
||||||
MapMarkerResponseDto,
|
MapMarkerResponseDto,
|
||||||
MemoryLaneResponseDto,
|
MemoryLaneResponseDto,
|
||||||
|
SanitizedAssetResponseDto,
|
||||||
TimeBucketResponseDto,
|
TimeBucketResponseDto,
|
||||||
mapAsset,
|
mapAsset,
|
||||||
} from './response-dto';
|
} from './response-dto';
|
||||||
|
@ -198,10 +199,17 @@ export class AssetService {
|
||||||
return this.assetRepository.getTimeBuckets(dto);
|
return this.assetRepository.getTimeBuckets(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByTimeBucket(authUser: AuthUserDto, dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
async getByTimeBucket(
|
||||||
|
authUser: AuthUserDto,
|
||||||
|
dto: TimeBucketAssetDto,
|
||||||
|
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
|
||||||
await this.timeBucketChecks(authUser, dto);
|
await this.timeBucketChecks(authUser, dto);
|
||||||
const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto);
|
const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto);
|
||||||
return assets.map(mapAsset);
|
if (authUser.isShowMetadata) {
|
||||||
|
return assets.map((asset) => mapAsset(asset));
|
||||||
|
} else {
|
||||||
|
return assets.map((asset) => mapAsset(asset, true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
|
async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
|
||||||
|
|
|
@ -6,43 +6,62 @@ import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.
|
||||||
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
||||||
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
|
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
|
||||||
|
|
||||||
export class AssetResponseDto {
|
export class SanitizedAssetResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
|
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
|
||||||
|
type!: AssetType;
|
||||||
|
thumbhash!: string | null;
|
||||||
|
resized!: boolean;
|
||||||
|
localDateTime!: Date;
|
||||||
|
duration!: string;
|
||||||
|
livePhotoVideoId?: string | null;
|
||||||
|
hasMetadata!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||||
deviceAssetId!: string;
|
deviceAssetId!: string;
|
||||||
deviceId!: string;
|
deviceId!: string;
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
owner?: UserResponseDto;
|
owner?: UserResponseDto;
|
||||||
libraryId!: string;
|
libraryId!: string;
|
||||||
|
|
||||||
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
|
|
||||||
type!: AssetType;
|
|
||||||
originalPath!: string;
|
originalPath!: string;
|
||||||
originalFileName!: string;
|
originalFileName!: string;
|
||||||
resized!: boolean;
|
resized!: boolean;
|
||||||
/**base64 encoded thumbhash */
|
|
||||||
thumbhash!: string | null;
|
|
||||||
fileCreatedAt!: Date;
|
fileCreatedAt!: Date;
|
||||||
fileModifiedAt!: Date;
|
fileModifiedAt!: Date;
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
isArchived!: boolean;
|
isArchived!: boolean;
|
||||||
isTrashed!: boolean;
|
isTrashed!: boolean;
|
||||||
localDateTime!: Date;
|
|
||||||
isOffline!: boolean;
|
isOffline!: boolean;
|
||||||
isExternal!: boolean;
|
isExternal!: boolean;
|
||||||
isReadOnly!: boolean;
|
isReadOnly!: boolean;
|
||||||
duration!: string;
|
|
||||||
exifInfo?: ExifResponseDto;
|
exifInfo?: ExifResponseDto;
|
||||||
smartInfo?: SmartInfoResponseDto;
|
smartInfo?: SmartInfoResponseDto;
|
||||||
livePhotoVideoId?: string | null;
|
|
||||||
tags?: TagResponseDto[];
|
tags?: TagResponseDto[];
|
||||||
people?: PersonResponseDto[];
|
people?: PersonResponseDto[];
|
||||||
/**base64 encoded sha1 hash */
|
/**base64 encoded sha1 hash */
|
||||||
checksum!: string;
|
checksum!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto {
|
export function mapAsset(entity: AssetEntity, stripMetadata = false): AssetResponseDto {
|
||||||
|
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
||||||
|
id: entity.id,
|
||||||
|
type: entity.type,
|
||||||
|
thumbhash: entity.thumbhash?.toString('base64') ?? null,
|
||||||
|
localDateTime: entity.localDateTime,
|
||||||
|
resized: !!entity.resizePath,
|
||||||
|
duration: entity.duration ?? '0:00:00.00000',
|
||||||
|
livePhotoVideoId: entity.livePhotoVideoId,
|
||||||
|
hasMetadata: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stripMetadata) {
|
||||||
|
return sanitizedAssetResponse as AssetResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...sanitizedAssetResponse,
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
deviceAssetId: entity.deviceAssetId,
|
deviceAssetId: entity.deviceAssetId,
|
||||||
ownerId: entity.ownerId,
|
ownerId: entity.ownerId,
|
||||||
|
@ -62,7 +81,7 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto {
|
||||||
isArchived: entity.isArchived,
|
isArchived: entity.isArchived,
|
||||||
isTrashed: !!entity.deletedAt,
|
isTrashed: !!entity.deletedAt,
|
||||||
duration: entity.duration ?? '0:00:00.00000',
|
duration: entity.duration ?? '0:00:00.00000',
|
||||||
exifInfo: withExif ? (entity.exifInfo ? mapExif(entity.exifInfo) : undefined) : undefined,
|
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
||||||
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
|
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
|
||||||
livePhotoVideoId: entity.livePhotoVideoId,
|
livePhotoVideoId: entity.livePhotoVideoId,
|
||||||
tags: entity.tags?.map(mapTag),
|
tags: entity.tags?.map(mapTag),
|
||||||
|
@ -71,17 +90,10 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto {
|
||||||
isExternal: entity.isExternal,
|
isExternal: entity.isExternal,
|
||||||
isOffline: entity.isOffline,
|
isOffline: entity.isOffline,
|
||||||
isReadOnly: entity.isReadOnly,
|
isReadOnly: entity.isReadOnly,
|
||||||
|
hasMetadata: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapAsset(entity: AssetEntity): AssetResponseDto {
|
|
||||||
return _map(entity, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
|
|
||||||
return _map(entity, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MemoryLaneResponseDto {
|
export class MemoryLaneResponseDto {
|
||||||
title!: string;
|
title!: string;
|
||||||
assets!: AssetResponseDto[];
|
assets!: AssetResponseDto[];
|
||||||
|
|
|
@ -52,3 +52,15 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||||
projectionType: entity.projectionType,
|
projectionType: entity.projectionType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto {
|
||||||
|
return {
|
||||||
|
fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null,
|
||||||
|
orientation: entity.orientation,
|
||||||
|
dateTimeOriginal: entity.dateTimeOriginal,
|
||||||
|
timeZone: entity.timeZone,
|
||||||
|
projectionType: entity.projectionType,
|
||||||
|
exifImageWidth: entity.exifImageWidth,
|
||||||
|
exifImageHeight: entity.exifImageHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -380,7 +380,7 @@ export class AuthService {
|
||||||
sharedLinkId: link.id,
|
sharedLinkId: link.id,
|
||||||
isAllowUpload: link.allowUpload,
|
isAllowUpload: link.allowUpload,
|
||||||
isAllowDownload: link.allowDownload,
|
isAllowDownload: link.allowDownload,
|
||||||
isShowExif: link.showExif,
|
isShowMetadata: link.showExif,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,7 +431,7 @@ export class AuthService {
|
||||||
isPublicUser: false,
|
isPublicUser: false,
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
isAllowDownload: true,
|
isAllowDownload: true,
|
||||||
isShowExif: true,
|
isShowMetadata: true,
|
||||||
accessTokenId: token.id,
|
accessTokenId: token.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ export class AuthUserDto {
|
||||||
sharedLinkId?: string;
|
sharedLinkId?: string;
|
||||||
isAllowUpload?: boolean;
|
isAllowUpload?: boolean;
|
||||||
isAllowDownload?: boolean;
|
isAllowDownload?: boolean;
|
||||||
isShowExif?: boolean;
|
isShowMetadata?: boolean;
|
||||||
accessTokenId?: string;
|
accessTokenId?: string;
|
||||||
externalPath?: string | null;
|
externalPath?: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ export class PersonService {
|
||||||
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> {
|
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> {
|
||||||
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
|
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
|
||||||
const assets = await this.repository.getAssets(id);
|
const assets = await this.repository.getAssets(id);
|
||||||
return assets.map(mapAsset);
|
return assets.map((asset) => mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||||
|
|
|
@ -154,7 +154,7 @@ export class SearchService {
|
||||||
items: assets.items
|
items: assets.items
|
||||||
.map((item) => lookup[item.id])
|
.map((item) => lookup[item.id])
|
||||||
.filter((item) => !!item)
|
.filter((item) => !!item)
|
||||||
.map(mapAsset),
|
.map((asset) => mapAsset(asset)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { SharedLinkEntity, SharedLinkType } from '@app/infra/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AlbumResponseDto, mapAlbumWithoutAssets } from '../album';
|
import { AlbumResponseDto, mapAlbumWithoutAssets } from '../album';
|
||||||
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../asset';
|
import { AssetResponseDto, mapAsset } from '../asset';
|
||||||
|
|
||||||
export class SharedLinkResponseDto {
|
export class SharedLinkResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
|
@ -17,8 +17,9 @@ export class SharedLinkResponseDto {
|
||||||
assets!: AssetResponseDto[];
|
assets!: AssetResponseDto[];
|
||||||
album?: AlbumResponseDto;
|
album?: AlbumResponseDto;
|
||||||
allowUpload!: boolean;
|
allowUpload!: boolean;
|
||||||
|
|
||||||
allowDownload!: boolean;
|
allowDownload!: boolean;
|
||||||
showExif!: boolean;
|
showMetadata!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||||
|
@ -35,15 +36,15 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
|
||||||
type: sharedLink.type,
|
type: sharedLink.type,
|
||||||
createdAt: sharedLink.createdAt,
|
createdAt: sharedLink.createdAt,
|
||||||
expiresAt: sharedLink.expiresAt,
|
expiresAt: sharedLink.expiresAt,
|
||||||
assets: assets.map(mapAsset),
|
assets: assets.map((asset) => mapAsset(asset)),
|
||||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||||
allowUpload: sharedLink.allowUpload,
|
allowUpload: sharedLink.allowUpload,
|
||||||
allowDownload: sharedLink.allowDownload,
|
allowDownload: sharedLink.allowDownload,
|
||||||
showExif: sharedLink.showExif,
|
showMetadata: sharedLink.showExif,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||||
const linkAssets = sharedLink.assets || [];
|
const linkAssets = sharedLink.assets || [];
|
||||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||||
|
|
||||||
|
@ -57,10 +58,10 @@ export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLin
|
||||||
type: sharedLink.type,
|
type: sharedLink.type,
|
||||||
createdAt: sharedLink.createdAt,
|
createdAt: sharedLink.createdAt,
|
||||||
expiresAt: sharedLink.expiresAt,
|
expiresAt: sharedLink.expiresAt,
|
||||||
assets: assets.map(mapAssetWithoutExif),
|
assets: assets.map((asset) => mapAsset(asset, true)) as AssetResponseDto[],
|
||||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||||
allowUpload: sharedLink.allowUpload,
|
allowUpload: sharedLink.allowUpload,
|
||||||
allowDownload: sharedLink.allowDownload,
|
allowDownload: sharedLink.allowDownload,
|
||||||
showExif: sharedLink.showExif,
|
showMetadata: sharedLink.showExif,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ export class SharedLinkCreateDto {
|
||||||
|
|
||||||
@Optional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
showExif?: boolean = true;
|
showMetadata?: boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SharedLinkEditDto {
|
export class SharedLinkEditDto {
|
||||||
|
@ -51,5 +51,5 @@ export class SharedLinkEditDto {
|
||||||
allowDownload?: boolean;
|
allowDownload?: boolean;
|
||||||
|
|
||||||
@Optional()
|
@Optional()
|
||||||
showExif?: boolean;
|
showMetadata?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,10 +59,10 @@ describe(SharedLinkService.name, () => {
|
||||||
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not return exif', async () => {
|
it('should not return metadata', async () => {
|
||||||
const authDto = authStub.adminSharedLinkNoExif;
|
const authDto = authStub.adminSharedLinkNoExif;
|
||||||
shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
||||||
await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.readonlyNoExif);
|
await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata);
|
||||||
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -137,7 +137,7 @@ describe(SharedLinkService.name, () => {
|
||||||
await sut.create(authStub.admin, {
|
await sut.create(authStub.admin, {
|
||||||
type: SharedLinkType.INDIVIDUAL,
|
type: SharedLinkType.INDIVIDUAL,
|
||||||
assetIds: [assetStub.image.id],
|
assetIds: [assetStub.image.id],
|
||||||
showExif: true,
|
showMetadata: true,
|
||||||
allowDownload: true,
|
allowDownload: true,
|
||||||
allowUpload: true,
|
allowUpload: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { AccessCore, Permission } from '../access';
|
||||||
import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset';
|
import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories';
|
import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories';
|
||||||
import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithNoExif } from './shared-link-response.dto';
|
import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto';
|
||||||
import { SharedLinkCreateDto, SharedLinkEditDto } from './shared-link.dto';
|
import { SharedLinkCreateDto, SharedLinkEditDto } from './shared-link.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -24,7 +24,7 @@ export class SharedLinkService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
|
async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
|
||||||
const { sharedLinkId: id, isPublicUser, isShowExif } = authUser;
|
const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser;
|
||||||
|
|
||||||
if (!isPublicUser || !id) {
|
if (!isPublicUser || !id) {
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
|
@ -69,7 +69,7 @@ export class SharedLinkService {
|
||||||
expiresAt: dto.expiresAt || null,
|
expiresAt: dto.expiresAt || null,
|
||||||
allowUpload: dto.allowUpload ?? true,
|
allowUpload: dto.allowUpload ?? true,
|
||||||
allowDownload: dto.allowDownload ?? true,
|
allowDownload: dto.allowDownload ?? true,
|
||||||
showExif: dto.showExif ?? true,
|
showExif: dto.showMetadata ?? true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.map(sharedLink, { withExif: true });
|
return this.map(sharedLink, { withExif: true });
|
||||||
|
@ -84,7 +84,7 @@ export class SharedLinkService {
|
||||||
expiresAt: dto.expiresAt,
|
expiresAt: dto.expiresAt,
|
||||||
allowUpload: dto.allowUpload,
|
allowUpload: dto.allowUpload,
|
||||||
allowDownload: dto.allowDownload,
|
allowDownload: dto.allowDownload,
|
||||||
showExif: dto.showExif,
|
showExif: dto.showMetadata,
|
||||||
});
|
});
|
||||||
return this.map(sharedLink, { withExif: true });
|
return this.map(sharedLink, { withExif: true });
|
||||||
}
|
}
|
||||||
|
@ -157,6 +157,6 @@ export class SharedLinkService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
||||||
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithNoExif(sharedLink);
|
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export class TagService {
|
||||||
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> {
|
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> {
|
||||||
await this.findOrFail(authUser, id);
|
await this.findOrFail(authUser, id);
|
||||||
const assets = await this.repository.getAssets(authUser.id, id);
|
const assets = await this.repository.getAssets(authUser.id, id);
|
||||||
return assets.map(mapAsset);
|
return assets.map((asset) => mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
|
async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
|
||||||
|
|
|
@ -186,7 +186,7 @@ export class AssetController {
|
||||||
@SharedLinkRoute()
|
@SharedLinkRoute()
|
||||||
@Get('/assetById/:id')
|
@Get('/assetById/:id')
|
||||||
getAssetById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
|
getAssetById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
|
||||||
return this.assetService.getAssetById(authUser, id);
|
return this.assetService.getAssetById(authUser, id) as Promise<AssetResponseDto>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,9 +10,9 @@ import {
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
JobName,
|
JobName,
|
||||||
mapAsset,
|
mapAsset,
|
||||||
mapAssetWithoutExif,
|
|
||||||
mimeTypes,
|
mimeTypes,
|
||||||
Permission,
|
Permission,
|
||||||
|
SanitizedAssetResponseDto,
|
||||||
UploadFile,
|
UploadFile,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
|
||||||
|
@ -187,12 +187,16 @@ export class AssetService {
|
||||||
return assets.map((asset) => mapAsset(asset));
|
return assets.map((asset) => mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
|
public async getAssetById(
|
||||||
|
authUser: AuthUserDto,
|
||||||
|
assetId: string,
|
||||||
|
): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
||||||
await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId);
|
await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId);
|
||||||
|
|
||||||
const allowExif = this.getExifPermission(authUser);
|
const includeMetadata = this.getExifPermission(authUser);
|
||||||
const asset = await this._assetRepository.getById(assetId);
|
const asset = await this._assetRepository.getById(assetId);
|
||||||
const data = allowExif ? mapAsset(asset) : mapAssetWithoutExif(asset);
|
if (includeMetadata) {
|
||||||
|
const data = mapAsset(asset);
|
||||||
|
|
||||||
if (data.ownerId !== authUser.id) {
|
if (data.ownerId !== authUser.id) {
|
||||||
data.people = [];
|
data.people = [];
|
||||||
|
@ -203,6 +207,9 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
} else {
|
||||||
|
return mapAsset(asset, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) {
|
async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) {
|
||||||
|
@ -374,7 +381,7 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
getExifPermission(authUser: AuthUserDto) {
|
getExifPermission(authUser: AuthUserDto) {
|
||||||
return !authUser.isPublicUser || authUser.isShowExif;
|
return !authUser.isPublicUser || authUser.isShowMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
|
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
|
||||||
|
|
|
@ -98,7 +98,7 @@ export class AssetController {
|
||||||
@Authenticated({ isShared: true })
|
@Authenticated({ isShared: true })
|
||||||
@Get('time-bucket')
|
@Get('time-bucket')
|
||||||
getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
||||||
return this.service.getByTimeBucket(authUser, dto);
|
return this.service.getByTimeBucket(authUser, dto) as Promise<AssetResponseDto[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('jobs')
|
@Post('jobs')
|
||||||
|
|
|
@ -10,4 +10,11 @@ export const sharedLinkApi = {
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
return body as SharedLinkResponseDto;
|
return body as SharedLinkResponseDto;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getMySharedLink: async (server: any, key: string) => {
|
||||||
|
const { status, body } = await request(server).get('/shared-link/me').query({ key });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as SharedLinkResponseDto;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9e6e1bcc245e0ae0285bb596faf310ead851fac6
|
Subproject commit 948f353e3c9b66156c86c86cf078e0746ec1598e
|
|
@ -1,11 +1,17 @@
|
||||||
import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain';
|
import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain';
|
||||||
import { PartnerController } from '@app/immich';
|
import { PartnerController } from '@app/immich';
|
||||||
import { SharedLinkType } from '@app/infra/entities';
|
import { LibraryType, SharedLinkType } from '@app/infra/entities';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { api } from '@test/api';
|
import { api } from '@test/api';
|
||||||
import { db } from '@test/db';
|
import { db } from '@test/db';
|
||||||
import { errorStub, uuidStub } from '@test/fixtures';
|
import { errorStub, uuidStub } from '@test/fixtures';
|
||||||
import { createTestApp } from '@test/test-utils';
|
import {
|
||||||
|
IMMICH_TEST_ASSET_PATH,
|
||||||
|
IMMICH_TEST_ASSET_TEMP_PATH,
|
||||||
|
createTestApp,
|
||||||
|
restoreTempFolder,
|
||||||
|
} from '@test/test-utils';
|
||||||
|
import { cp } from 'fs/promises';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
const user1Dto = {
|
const user1Dto = {
|
||||||
|
@ -18,24 +24,22 @@ const user1Dto = {
|
||||||
describe(`${PartnerController.name} (e2e)`, () => {
|
describe(`${PartnerController.name} (e2e)`, () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let server: any;
|
let server: any;
|
||||||
let loginResponse: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let accessToken: string;
|
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
let sharedLink: SharedLinkResponseDto;
|
let sharedLink: SharedLinkResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await createTestApp();
|
app = await createTestApp(true);
|
||||||
server = app.getHttpServer();
|
server = app.getHttpServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await db.reset();
|
await db.reset();
|
||||||
await api.authApi.adminSignUp(server);
|
await api.authApi.adminSignUp(server);
|
||||||
loginResponse = await api.authApi.adminLogin(server);
|
admin = await api.authApi.adminLogin(server);
|
||||||
accessToken = loginResponse.accessToken;
|
|
||||||
|
|
||||||
await api.userApi.create(server, accessToken, user1Dto);
|
await api.userApi.create(server, admin.accessToken, user1Dto);
|
||||||
user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password });
|
user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password });
|
||||||
|
|
||||||
album = await api.albumApi.create(server, user1.accessToken, { albumName: 'shared with link' });
|
album = await api.albumApi.create(server, user1.accessToken, { albumName: 'shared with link' });
|
||||||
|
@ -48,6 +52,7 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await db.disconnect();
|
await db.disconnect();
|
||||||
await app.close();
|
await app.close();
|
||||||
|
await restoreTempFolder();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /shared-link', () => {
|
describe('GET /shared-link', () => {
|
||||||
|
@ -68,7 +73,9 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not get shared links created by other users', async () => {
|
it('should not get shared links created by other users', async () => {
|
||||||
const { status, body } = await request(server).get('/shared-link').set('Authorization', `Bearer ${accessToken}`);
|
const { status, body } = await request(server)
|
||||||
|
.get('/shared-link')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([]);
|
expect(body).toEqual([]);
|
||||||
|
@ -77,7 +84,9 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||||
|
|
||||||
describe('GET /shared-link/me', () => {
|
describe('GET /shared-link/me', () => {
|
||||||
it('should not require admin authentication', async () => {
|
it('should not require admin authentication', async () => {
|
||||||
const { status } = await request(server).get('/shared-link/me').set('Authorization', `Bearer ${accessToken}`);
|
const { status } = await request(server)
|
||||||
|
.get('/shared-link/me')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(403);
|
expect(status).toBe(403);
|
||||||
});
|
});
|
||||||
|
@ -104,7 +113,7 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||||
type: SharedLinkType.ALBUM,
|
type: SharedLinkType.ALBUM,
|
||||||
albumId: softDeletedAlbum.id,
|
albumId: softDeletedAlbum.id,
|
||||||
});
|
});
|
||||||
await api.userApi.delete(server, accessToken, user1.userId);
|
await api.userApi.delete(server, admin.accessToken, user1.userId);
|
||||||
|
|
||||||
const { status, body } = await request(server).get('/shared-link/me').query({ key: softDeletedAlbumLink.key });
|
const { status, body } = await request(server).get('/shared-link/me').query({ key: softDeletedAlbumLink.key });
|
||||||
|
|
||||||
|
@ -133,7 +142,7 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||||
it('should not get shared link by id if user has not created the link or it does not exist', async () => {
|
it('should not get shared link by id if user has not created the link or it does not exist', async () => {
|
||||||
const { status, body } = await request(server)
|
const { status, body } = await request(server)
|
||||||
.get(`/shared-link/${sharedLink.id}`)
|
.get(`/shared-link/${sharedLink.id}`)
|
||||||
.set('Authorization', `Bearer ${accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
|
expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
|
||||||
|
@ -248,4 +257,81 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Shared link metadata', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await restoreTempFolder();
|
||||||
|
|
||||||
|
await cp(
|
||||||
|
`${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`,
|
||||||
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/thompson-springs.jpg`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||||
|
|
||||||
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||||
|
type: LibraryType.EXTERNAL,
|
||||||
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
|
||||||
|
album = await api.albumApi.create(server, admin.accessToken, { albumName: 'New album' });
|
||||||
|
await api.albumApi.addAssets(server, admin.accessToken, album.id, { ids: [assets[0].id] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return metadata for album shared link', async () => {
|
||||||
|
const sharedLink = await api.sharedLinkApi.create(server, admin.accessToken, {
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
albumId: album.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const returnedLink = await api.sharedLinkApi.getMySharedLink(server, sharedLink.key);
|
||||||
|
|
||||||
|
expect(returnedLink.assets).toHaveLength(1);
|
||||||
|
expect(returnedLink.album).toBeDefined();
|
||||||
|
|
||||||
|
const returnedAsset = returnedLink.assets[0];
|
||||||
|
expect(returnedAsset).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'thompson-springs',
|
||||||
|
resized: true,
|
||||||
|
localDateTime: '2022-01-10T15:15:44.310Z',
|
||||||
|
fileCreatedAt: '2022-01-10T19:15:44.310Z',
|
||||||
|
exifInfo: expect.objectContaining({
|
||||||
|
longitude: -108.400968333333,
|
||||||
|
latitude: 39.115,
|
||||||
|
orientation: '1',
|
||||||
|
dateTimeOriginal: '2022-01-10T19:15:44.310Z',
|
||||||
|
timeZone: 'UTC-4',
|
||||||
|
state: 'Mesa County, Colorado',
|
||||||
|
country: 'United States of America',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return metadata for album shared link without metadata', async () => {
|
||||||
|
const sharedLink = await api.sharedLinkApi.create(server, admin.accessToken, {
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
albumId: album.id,
|
||||||
|
showMetadata: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const returnedLink = await api.sharedLinkApi.getMySharedLink(server, sharedLink.key);
|
||||||
|
|
||||||
|
expect(returnedLink.assets).toHaveLength(1);
|
||||||
|
expect(returnedLink.album).toBeDefined();
|
||||||
|
|
||||||
|
const returnedAsset = returnedLink.assets[0];
|
||||||
|
expect(returnedAsset).not.toHaveProperty('exifInfo');
|
||||||
|
expect(returnedAsset).not.toHaveProperty('fileCreatedAt');
|
||||||
|
expect(returnedAsset).not.toHaveProperty('originalFilename');
|
||||||
|
expect(returnedAsset).not.toHaveProperty('originalPath');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
12
server/test/fixtures/auth.stub.ts
vendored
12
server/test/fixtures/auth.stub.ts
vendored
|
@ -48,7 +48,7 @@ export const authStub = {
|
||||||
isPublicUser: false,
|
isPublicUser: false,
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
isAllowDownload: true,
|
isAllowDownload: true,
|
||||||
isShowExif: true,
|
isShowMetadata: true,
|
||||||
accessTokenId: 'token-id',
|
accessTokenId: 'token-id',
|
||||||
externalPath: null,
|
externalPath: null,
|
||||||
}),
|
}),
|
||||||
|
@ -59,7 +59,7 @@ export const authStub = {
|
||||||
isPublicUser: false,
|
isPublicUser: false,
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
isAllowDownload: true,
|
isAllowDownload: true,
|
||||||
isShowExif: true,
|
isShowMetadata: true,
|
||||||
accessTokenId: 'token-id',
|
accessTokenId: 'token-id',
|
||||||
externalPath: null,
|
externalPath: null,
|
||||||
}),
|
}),
|
||||||
|
@ -70,7 +70,7 @@ export const authStub = {
|
||||||
isPublicUser: false,
|
isPublicUser: false,
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
isAllowDownload: true,
|
isAllowDownload: true,
|
||||||
isShowExif: true,
|
isShowMetadata: true,
|
||||||
accessTokenId: 'token-id',
|
accessTokenId: 'token-id',
|
||||||
externalPath: '/data/user1',
|
externalPath: '/data/user1',
|
||||||
}),
|
}),
|
||||||
|
@ -81,7 +81,7 @@ export const authStub = {
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
isAllowDownload: true,
|
isAllowDownload: true,
|
||||||
isPublicUser: true,
|
isPublicUser: true,
|
||||||
isShowExif: true,
|
isShowMetadata: true,
|
||||||
sharedLinkId: '123',
|
sharedLinkId: '123',
|
||||||
}),
|
}),
|
||||||
adminSharedLinkNoExif: Object.freeze<AuthUserDto>({
|
adminSharedLinkNoExif: Object.freeze<AuthUserDto>({
|
||||||
|
@ -91,7 +91,7 @@ export const authStub = {
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
isAllowDownload: true,
|
isAllowDownload: true,
|
||||||
isPublicUser: true,
|
isPublicUser: true,
|
||||||
isShowExif: false,
|
isShowMetadata: false,
|
||||||
sharedLinkId: '123',
|
sharedLinkId: '123',
|
||||||
}),
|
}),
|
||||||
readonlySharedLink: Object.freeze<AuthUserDto>({
|
readonlySharedLink: Object.freeze<AuthUserDto>({
|
||||||
|
@ -101,7 +101,7 @@ export const authStub = {
|
||||||
isAllowUpload: false,
|
isAllowUpload: false,
|
||||||
isAllowDownload: false,
|
isAllowDownload: false,
|
||||||
isPublicUser: true,
|
isPublicUser: true,
|
||||||
isShowExif: true,
|
isShowMetadata: true,
|
||||||
sharedLinkId: '123',
|
sharedLinkId: '123',
|
||||||
accessTokenId: 'token-id',
|
accessTokenId: 'token-id',
|
||||||
}),
|
}),
|
||||||
|
|
24
server/test/fixtures/shared-link.stub.ts
vendored
24
server/test/fixtures/shared-link.stub.ts
vendored
|
@ -71,8 +71,20 @@ const assetResponse: AssetResponseDto = {
|
||||||
checksum: 'ZmlsZSBoYXNo',
|
checksum: 'ZmlsZSBoYXNo',
|
||||||
isTrashed: false,
|
isTrashed: false,
|
||||||
libraryId: 'library-id',
|
libraryId: 'library-id',
|
||||||
|
hasMetadata: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const assetResponseWithoutMetadata = {
|
||||||
|
id: 'id_1',
|
||||||
|
type: AssetType.VIDEO,
|
||||||
|
resized: false,
|
||||||
|
thumbhash: null,
|
||||||
|
localDateTime: today,
|
||||||
|
duration: '0:00:00.00000',
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
hasMetadata: false,
|
||||||
|
} as AssetResponseDto;
|
||||||
|
|
||||||
const albumResponse: AlbumResponseDto = {
|
const albumResponse: AlbumResponseDto = {
|
||||||
albumName: 'Test Album',
|
albumName: 'Test Album',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -253,7 +265,7 @@ export const sharedLinkResponseStub = {
|
||||||
expiresAt: tomorrow,
|
expiresAt: tomorrow,
|
||||||
id: '123',
|
id: '123',
|
||||||
key: sharedLinkBytes.toString('base64url'),
|
key: sharedLinkBytes.toString('base64url'),
|
||||||
showExif: true,
|
showMetadata: true,
|
||||||
type: SharedLinkType.ALBUM,
|
type: SharedLinkType.ALBUM,
|
||||||
userId: 'admin_id',
|
userId: 'admin_id',
|
||||||
}),
|
}),
|
||||||
|
@ -267,7 +279,7 @@ export const sharedLinkResponseStub = {
|
||||||
expiresAt: yesterday,
|
expiresAt: yesterday,
|
||||||
id: '123',
|
id: '123',
|
||||||
key: sharedLinkBytes.toString('base64url'),
|
key: sharedLinkBytes.toString('base64url'),
|
||||||
showExif: true,
|
showMetadata: true,
|
||||||
type: SharedLinkType.ALBUM,
|
type: SharedLinkType.ALBUM,
|
||||||
userId: 'admin_id',
|
userId: 'admin_id',
|
||||||
}),
|
}),
|
||||||
|
@ -281,11 +293,11 @@ export const sharedLinkResponseStub = {
|
||||||
description: null,
|
description: null,
|
||||||
allowUpload: false,
|
allowUpload: false,
|
||||||
allowDownload: false,
|
allowDownload: false,
|
||||||
showExif: true,
|
showMetadata: true,
|
||||||
album: albumResponse,
|
album: albumResponse,
|
||||||
assets: [assetResponse],
|
assets: [assetResponse],
|
||||||
}),
|
}),
|
||||||
readonlyNoExif: Object.freeze<SharedLinkResponseDto>({
|
readonlyNoMetadata: Object.freeze<SharedLinkResponseDto>({
|
||||||
id: '123',
|
id: '123',
|
||||||
userId: 'admin_id',
|
userId: 'admin_id',
|
||||||
key: sharedLinkBytes.toString('base64url'),
|
key: sharedLinkBytes.toString('base64url'),
|
||||||
|
@ -295,8 +307,8 @@ export const sharedLinkResponseStub = {
|
||||||
description: null,
|
description: null,
|
||||||
allowUpload: false,
|
allowUpload: false,
|
||||||
allowDownload: false,
|
allowDownload: false,
|
||||||
showExif: false,
|
showMetadata: false,
|
||||||
album: { ...albumResponse, startDate: assetResponse.fileCreatedAt, endDate: assetResponse.fileCreatedAt },
|
album: { ...albumResponse, startDate: assetResponse.fileCreatedAt, endDate: assetResponse.fileCreatedAt },
|
||||||
assets: [{ ...assetResponse, exifInfo: undefined }],
|
assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
14
web/src/api/open-api/api.ts
generated
14
web/src/api/open-api/api.ts
generated
|
@ -640,6 +640,12 @@ export interface AssetResponseDto {
|
||||||
* @memberof AssetResponseDto
|
* @memberof AssetResponseDto
|
||||||
*/
|
*/
|
||||||
'fileModifiedAt': string;
|
'fileModifiedAt': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetResponseDto
|
||||||
|
*/
|
||||||
|
'hasMetadata': boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -749,7 +755,7 @@ export interface AssetResponseDto {
|
||||||
*/
|
*/
|
||||||
'tags'?: Array<TagResponseDto>;
|
'tags'?: Array<TagResponseDto>;
|
||||||
/**
|
/**
|
||||||
* base64 encoded thumbhash
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AssetResponseDto
|
* @memberof AssetResponseDto
|
||||||
*/
|
*/
|
||||||
|
@ -2882,7 +2888,7 @@ export interface SharedLinkCreateDto {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof SharedLinkCreateDto
|
* @memberof SharedLinkCreateDto
|
||||||
*/
|
*/
|
||||||
'showExif'?: boolean;
|
'showMetadata'?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SharedLinkType}
|
* @type {SharedLinkType}
|
||||||
|
@ -2927,7 +2933,7 @@ export interface SharedLinkEditDto {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof SharedLinkEditDto
|
* @memberof SharedLinkEditDto
|
||||||
*/
|
*/
|
||||||
'showExif'?: boolean;
|
'showMetadata'?: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -2994,7 +3000,7 @@ export interface SharedLinkResponseDto {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof SharedLinkResponseDto
|
* @memberof SharedLinkResponseDto
|
||||||
*/
|
*/
|
||||||
'showExif': boolean;
|
'showMetadata': boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SharedLinkType}
|
* @type {SharedLinkType}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
export let showMotionPlayButton: boolean;
|
export let showMotionPlayButton: boolean;
|
||||||
export let isMotionPhotoPlaying = false;
|
export let isMotionPhotoPlaying = false;
|
||||||
export let showDownloadButton: boolean;
|
export let showDownloadButton: boolean;
|
||||||
|
export let showDetailButton: boolean;
|
||||||
export let showSlideshow = false;
|
export let showSlideshow = false;
|
||||||
|
|
||||||
const isOwner = asset.ownerId === $page.data.user?.id;
|
const isOwner = asset.ownerId === $page.data.user?.id;
|
||||||
|
@ -133,7 +134,14 @@
|
||||||
title="Download"
|
title="Download"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<CircleIconButton isOpacity={true} logo={InformationOutline} on:click={() => dispatch('showDetail')} title="Info" />
|
{#if showDetailButton}
|
||||||
|
<CircleIconButton
|
||||||
|
isOpacity={true}
|
||||||
|
logo={InformationOutline}
|
||||||
|
on:click={() => dispatch('showDetail')}
|
||||||
|
title="Info"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
isOpacity={true}
|
isOpacity={true}
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
let shouldPlayMotionPhoto = false;
|
let shouldPlayMotionPhoto = false;
|
||||||
let isShowProfileImageCrop = false;
|
let isShowProfileImageCrop = false;
|
||||||
let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
|
let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
|
||||||
|
let shouldShowDetailButton = asset.hasMetadata;
|
||||||
let canCopyImagesToClipboard: boolean;
|
let canCopyImagesToClipboard: boolean;
|
||||||
|
|
||||||
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo);
|
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo);
|
||||||
|
@ -392,6 +393,7 @@
|
||||||
showZoomButton={asset.type === AssetTypeEnum.Image}
|
showZoomButton={asset.type === AssetTypeEnum.Image}
|
||||||
showMotionPlayButton={!!asset.livePhotoVideoId}
|
showMotionPlayButton={!!asset.livePhotoVideoId}
|
||||||
showDownloadButton={shouldShowDownloadButton}
|
showDownloadButton={shouldShowDownloadButton}
|
||||||
|
showDetailButton={shouldShowDetailButton}
|
||||||
showSlideshow={!!assetStore}
|
showSlideshow={!!assetStore}
|
||||||
on:goBack={closeViewer}
|
on:goBack={closeViewer}
|
||||||
on:showDetail={showDetailInfoHandler}
|
on:showDetail={showDetailInfoHandler}
|
||||||
|
@ -433,9 +435,9 @@
|
||||||
on:close={closeViewer}
|
on:close={closeViewer}
|
||||||
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
||||||
/>
|
/>
|
||||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || asset.originalPath
|
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.endsWith('.insp')}
|
.endsWith('.insp'))}
|
||||||
<PanoramaViewer {asset} />
|
<PanoramaViewer {asset} />
|
||||||
{:else}
|
{:else}
|
||||||
<PhotoViewer {asset} on:close={closeViewer} />
|
<PhotoViewer {asset} on:close={closeViewer} />
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
let description = '';
|
let description = '';
|
||||||
let allowDownload = true;
|
let allowDownload = true;
|
||||||
let allowUpload = false;
|
let allowUpload = false;
|
||||||
let showExif = true;
|
let showMetadata = true;
|
||||||
let expirationTime = '';
|
let expirationTime = '';
|
||||||
let shouldChangeExpirationTime = false;
|
let shouldChangeExpirationTime = false;
|
||||||
let canCopyImagesToClipboard = true;
|
let canCopyImagesToClipboard = true;
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
}
|
}
|
||||||
allowUpload = editingLink.allowUpload;
|
allowUpload = editingLink.allowUpload;
|
||||||
allowDownload = editingLink.allowDownload;
|
allowDownload = editingLink.allowDownload;
|
||||||
showExif = editingLink.showExif;
|
showMetadata = editingLink.showMetadata;
|
||||||
|
|
||||||
albumId = editingLink.album?.id;
|
albumId = editingLink.album?.id;
|
||||||
assetIds = editingLink.assets.map(({ id }) => id);
|
assetIds = editingLink.assets.map(({ id }) => id);
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
allowUpload,
|
allowUpload,
|
||||||
description,
|
description,
|
||||||
allowDownload,
|
allowDownload,
|
||||||
showExif,
|
showMetadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
sharedLink = `${window.location.origin}/share/${data.key}`;
|
sharedLink = `${window.location.origin}/share/${data.key}`;
|
||||||
|
@ -119,9 +119,9 @@
|
||||||
sharedLinkEditDto: {
|
sharedLinkEditDto: {
|
||||||
description,
|
description,
|
||||||
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||||
allowUpload: allowUpload,
|
allowUpload,
|
||||||
allowDownload: allowDownload,
|
allowDownload,
|
||||||
showExif: showExif,
|
showMetadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<SettingSwitch bind:checked={showExif} title={'Show metadata'} />
|
<SettingSwitch bind:checked={showMetadata} title={'Show metadata'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
|
|
|
@ -136,7 +136,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if link.showExif}
|
{#if link.showMetadata}
|
||||||
<div
|
<div
|
||||||
class="flex w-[60px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
|
class="flex w-[60px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in a new issue