From dadcf49ecaf601677789ee67c9d0047115b95d32 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Sat, 14 Oct 2023 03:46:30 +0200 Subject: [PATCH] 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 --- cli/src/api/open-api/api.ts | 14 ++- mobile/openapi/doc/AssetResponseDto.md | 3 +- mobile/openapi/doc/SharedLinkCreateDto.md | 2 +- mobile/openapi/doc/SharedLinkEditDto.md | 2 +- mobile/openapi/doc/SharedLinkResponseDto.md | 2 +- .../openapi/lib/model/asset_response_dto.dart | 11 +- .../lib/model/shared_link_create_dto.dart | 14 +-- .../lib/model/shared_link_edit_dto.dart | 18 +-- .../lib/model/shared_link_response_dto.dart | 16 +-- .../openapi/test/asset_response_dto_test.dart | 6 +- .../test/shared_link_create_dto_test.dart | 4 +- .../test/shared_link_edit_dto_test.dart | 4 +- .../test/shared_link_response_dto_test.dart | 4 +- server/immich-openapi-specs.json | 21 ++-- server/src/domain/asset/asset.service.ts | 12 +- .../asset/response-dto/asset-response.dto.ts | 50 +++++--- .../asset/response-dto/exif-response.dto.ts | 12 ++ server/src/domain/auth/auth.service.ts | 4 +- server/src/domain/auth/dto/auth-user.dto.ts | 2 +- server/src/domain/person/person.service.ts | 2 +- server/src/domain/search/search.service.ts | 2 +- .../shared-link/shared-link-response.dto.ts | 15 +-- .../src/domain/shared-link/shared-link.dto.ts | 4 +- .../shared-link/shared-link.service.spec.ts | 6 +- .../domain/shared-link/shared-link.service.ts | 10 +- server/src/domain/tag/tag.service.ts | 2 +- .../immich/api-v1/asset/asset.controller.ts | 2 +- .../src/immich/api-v1/asset/asset.service.ts | 33 +++--- .../immich/controllers/asset.controller.ts | 2 +- server/test/api/shared-link-api.ts | 7 ++ server/test/assets | 2 +- server/test/e2e/shared-link.e2e-spec.ts | 110 ++++++++++++++++-- server/test/fixtures/auth.stub.ts | 12 +- server/test/fixtures/shared-link.stub.ts | 24 +++- web/src/api/open-api/api.ts | 14 ++- .../asset-viewer/asset-viewer-nav-bar.svelte | 10 +- .../asset-viewer/asset-viewer.svelte | 8 +- .../create-shared-link-modal.svelte | 14 +-- .../sharedlinks-page/shared-link-card.svelte | 2 +- 39 files changed, 332 insertions(+), 150 deletions(-) diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 2be639a0f1..0c8d2673cc 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -640,6 +640,12 @@ export interface AssetResponseDto { * @memberof AssetResponseDto */ 'fileModifiedAt': string; + /** + * + * @type {boolean} + * @memberof AssetResponseDto + */ + 'hasMetadata': boolean; /** * * @type {string} @@ -749,7 +755,7 @@ export interface AssetResponseDto { */ 'tags'?: Array; /** - * base64 encoded thumbhash + * * @type {string} * @memberof AssetResponseDto */ @@ -2882,7 +2888,7 @@ export interface SharedLinkCreateDto { * @type {boolean} * @memberof SharedLinkCreateDto */ - 'showExif'?: boolean; + 'showMetadata'?: boolean; /** * * @type {SharedLinkType} @@ -2927,7 +2933,7 @@ export interface SharedLinkEditDto { * @type {boolean} * @memberof SharedLinkEditDto */ - 'showExif'?: boolean; + 'showMetadata'?: boolean; } /** * @@ -2994,7 +3000,7 @@ export interface SharedLinkResponseDto { * @type {boolean} * @memberof SharedLinkResponseDto */ - 'showExif': boolean; + 'showMetadata': boolean; /** * * @type {SharedLinkType} diff --git a/mobile/openapi/doc/AssetResponseDto.md b/mobile/openapi/doc/AssetResponseDto.md index 2fd91061f9..a08be71ace 100644 --- a/mobile/openapi/doc/AssetResponseDto.md +++ b/mobile/openapi/doc/AssetResponseDto.md @@ -15,6 +15,7 @@ Name | Type | Description | Notes **exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) | | [optional] **fileCreatedAt** | [**DateTime**](DateTime.md) | | **fileModifiedAt** | [**DateTime**](DateTime.md) | | +**hasMetadata** | **bool** | | **id** | **String** | | **isArchived** | **bool** | | **isExternal** | **bool** | | @@ -33,7 +34,7 @@ Name | Type | Description | Notes **resized** | **bool** | | **smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional] **tags** | [**List**](TagResponseDto.md) | | [optional] [default to const []] -**thumbhash** | **String** | base64 encoded thumbhash | +**thumbhash** | **String** | | **type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | **updatedAt** | [**DateTime**](DateTime.md) | | diff --git a/mobile/openapi/doc/SharedLinkCreateDto.md b/mobile/openapi/doc/SharedLinkCreateDto.md index 807a8d18e7..852610ae12 100644 --- a/mobile/openapi/doc/SharedLinkCreateDto.md +++ b/mobile/openapi/doc/SharedLinkCreateDto.md @@ -14,7 +14,7 @@ Name | Type | Description | Notes **assetIds** | **List** | | [optional] [default to const []] **description** | **String** | | [optional] **expiresAt** | [**DateTime**](DateTime.md) | | [optional] -**showExif** | **bool** | | [optional] [default to true] +**showMetadata** | **bool** | | [optional] [default to true] **type** | [**SharedLinkType**](SharedLinkType.md) | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/SharedLinkEditDto.md b/mobile/openapi/doc/SharedLinkEditDto.md index 387ef4c503..f035e23c63 100644 --- a/mobile/openapi/doc/SharedLinkEditDto.md +++ b/mobile/openapi/doc/SharedLinkEditDto.md @@ -12,7 +12,7 @@ Name | Type | Description | Notes **allowUpload** | **bool** | | [optional] **description** | **String** | | [optional] **expiresAt** | [**DateTime**](DateTime.md) | | [optional] -**showExif** | **bool** | | [optional] +**showMetadata** | **bool** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/SharedLinkResponseDto.md b/mobile/openapi/doc/SharedLinkResponseDto.md index 3e7c8256b2..24b76c86c8 100644 --- a/mobile/openapi/doc/SharedLinkResponseDto.md +++ b/mobile/openapi/doc/SharedLinkResponseDto.md @@ -17,7 +17,7 @@ Name | Type | Description | Notes **expiresAt** | [**DateTime**](DateTime.md) | | **id** | **String** | | **key** | **String** | | -**showExif** | **bool** | | +**showMetadata** | **bool** | | **type** | [**SharedLinkType**](SharedLinkType.md) | | **userId** | **String** | | diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index f127b5d2c3..b2feb0ee82 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -20,6 +20,7 @@ class AssetResponseDto { this.exifInfo, required this.fileCreatedAt, required this.fileModifiedAt, + required this.hasMetadata, required this.id, required this.isArchived, required this.isExternal, @@ -64,6 +65,8 @@ class AssetResponseDto { DateTime fileModifiedAt; + bool hasMetadata; + String id; bool isArchived; @@ -112,7 +115,6 @@ class AssetResponseDto { List tags; - /// base64 encoded thumbhash String? thumbhash; AssetTypeEnum type; @@ -128,6 +130,7 @@ class AssetResponseDto { other.exifInfo == exifInfo && other.fileCreatedAt == fileCreatedAt && other.fileModifiedAt == fileModifiedAt && + other.hasMetadata == hasMetadata && other.id == id && other.isArchived == isArchived && other.isExternal == isExternal && @@ -160,6 +163,7 @@ class AssetResponseDto { (exifInfo == null ? 0 : exifInfo!.hashCode) + (fileCreatedAt.hashCode) + (fileModifiedAt.hashCode) + + (hasMetadata.hashCode) + (id.hashCode) + (isArchived.hashCode) + (isExternal.hashCode) + @@ -183,7 +187,7 @@ class AssetResponseDto { (updatedAt.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]'; + String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -198,6 +202,7 @@ class AssetResponseDto { } json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); + json[r'hasMetadata'] = this.hasMetadata; json[r'id'] = this.id; json[r'isArchived'] = this.isArchived; json[r'isExternal'] = this.isExternal; @@ -253,6 +258,7 @@ class AssetResponseDto { exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), fileCreatedAt: mapDateTime(json, r'fileCreatedAt', '')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', '')!, + hasMetadata: mapValueOfType(json, r'hasMetadata')!, id: mapValueOfType(json, r'id')!, isArchived: mapValueOfType(json, r'isArchived')!, isExternal: mapValueOfType(json, r'isExternal')!, @@ -327,6 +333,7 @@ class AssetResponseDto { 'duration', 'fileCreatedAt', 'fileModifiedAt', + 'hasMetadata', 'id', 'isArchived', 'isExternal', diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index a940808c1a..8ce045ca1a 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -19,7 +19,7 @@ class SharedLinkCreateDto { this.assetIds = const [], this.description, this.expiresAt, - this.showExif = true, + this.showMetadata = true, required this.type, }); @@ -47,7 +47,7 @@ class SharedLinkCreateDto { DateTime? expiresAt; - bool showExif; + bool showMetadata; SharedLinkType type; @@ -59,7 +59,7 @@ class SharedLinkCreateDto { other.assetIds == assetIds && other.description == description && other.expiresAt == expiresAt && - other.showExif == showExif && + other.showMetadata == showMetadata && other.type == type; @override @@ -71,11 +71,11 @@ class SharedLinkCreateDto { (assetIds.hashCode) + (description == null ? 0 : description!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + - (showExif.hashCode) + + (showMetadata.hashCode) + (type.hashCode); @override - String toString() => 'SharedLinkCreateDto[albumId=$albumId, allowDownload=$allowDownload, allowUpload=$allowUpload, assetIds=$assetIds, description=$description, expiresAt=$expiresAt, showExif=$showExif, type=$type]'; + String toString() => 'SharedLinkCreateDto[albumId=$albumId, allowDownload=$allowDownload, allowUpload=$allowUpload, assetIds=$assetIds, description=$description, expiresAt=$expiresAt, showMetadata=$showMetadata, type=$type]'; Map toJson() { final json = {}; @@ -97,7 +97,7 @@ class SharedLinkCreateDto { } else { // json[r'expiresAt'] = null; } - json[r'showExif'] = this.showExif; + json[r'showMetadata'] = this.showMetadata; json[r'type'] = this.type; return json; } @@ -118,7 +118,7 @@ class SharedLinkCreateDto { : const [], description: mapValueOfType(json, r'description'), expiresAt: mapDateTime(json, r'expiresAt', ''), - showExif: mapValueOfType(json, r'showExif') ?? true, + showMetadata: mapValueOfType(json, r'showMetadata') ?? true, type: SharedLinkType.fromJson(json[r'type'])!, ); } diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index fb79361557..6b72e025db 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -17,7 +17,7 @@ class SharedLinkEditDto { this.allowUpload, this.description, this.expiresAt, - this.showExif, + this.showMetadata, }); /// @@ -52,7 +52,7 @@ class SharedLinkEditDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - bool? showExif; + bool? showMetadata; @override bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto && @@ -60,7 +60,7 @@ class SharedLinkEditDto { other.allowUpload == allowUpload && other.description == description && other.expiresAt == expiresAt && - other.showExif == showExif; + other.showMetadata == showMetadata; @override int get hashCode => @@ -69,10 +69,10 @@ class SharedLinkEditDto { (allowUpload == null ? 0 : allowUpload!.hashCode) + (description == null ? 0 : description!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + - (showExif == null ? 0 : showExif!.hashCode); + (showMetadata == null ? 0 : showMetadata!.hashCode); @override - String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, expiresAt=$expiresAt, showExif=$showExif]'; + String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, expiresAt=$expiresAt, showMetadata=$showMetadata]'; Map toJson() { final json = {}; @@ -96,10 +96,10 @@ class SharedLinkEditDto { } else { // json[r'expiresAt'] = null; } - if (this.showExif != null) { - json[r'showExif'] = this.showExif; + if (this.showMetadata != null) { + json[r'showMetadata'] = this.showMetadata; } else { - // json[r'showExif'] = null; + // json[r'showMetadata'] = null; } return json; } @@ -116,7 +116,7 @@ class SharedLinkEditDto { allowUpload: mapValueOfType(json, r'allowUpload'), description: mapValueOfType(json, r'description'), expiresAt: mapDateTime(json, r'expiresAt', ''), - showExif: mapValueOfType(json, r'showExif'), + showMetadata: mapValueOfType(json, r'showMetadata'), ); } return null; diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index 72a7299a46..33aa0577d7 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -22,7 +22,7 @@ class SharedLinkResponseDto { required this.expiresAt, required this.id, required this.key, - required this.showExif, + required this.showMetadata, required this.type, required this.userId, }); @@ -51,7 +51,7 @@ class SharedLinkResponseDto { String key; - bool showExif; + bool showMetadata; SharedLinkType type; @@ -68,7 +68,7 @@ class SharedLinkResponseDto { other.expiresAt == expiresAt && other.id == id && other.key == key && - other.showExif == showExif && + other.showMetadata == showMetadata && other.type == type && other.userId == userId; @@ -84,12 +84,12 @@ class SharedLinkResponseDto { (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + (key.hashCode) + - (showExif.hashCode) + + (showMetadata.hashCode) + (type.hashCode) + (userId.hashCode); @override - String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, showExif=$showExif, type=$type, userId=$userId]'; + String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, showMetadata=$showMetadata, type=$type, userId=$userId]'; Map toJson() { final json = {}; @@ -114,7 +114,7 @@ class SharedLinkResponseDto { } json[r'id'] = this.id; json[r'key'] = this.key; - json[r'showExif'] = this.showExif; + json[r'showMetadata'] = this.showMetadata; json[r'type'] = this.type; json[r'userId'] = this.userId; return json; @@ -137,7 +137,7 @@ class SharedLinkResponseDto { expiresAt: mapDateTime(json, r'expiresAt', ''), id: mapValueOfType(json, r'id')!, key: mapValueOfType(json, r'key')!, - showExif: mapValueOfType(json, r'showExif')!, + showMetadata: mapValueOfType(json, r'showMetadata')!, type: SharedLinkType.fromJson(json[r'type'])!, userId: mapValueOfType(json, r'userId')!, ); @@ -195,7 +195,7 @@ class SharedLinkResponseDto { 'expiresAt', 'id', 'key', - 'showExif', + 'showMetadata', 'type', 'userId', }; diff --git a/mobile/openapi/test/asset_response_dto_test.dart b/mobile/openapi/test/asset_response_dto_test.dart index a5f9722010..f450aae274 100644 --- a/mobile/openapi/test/asset_response_dto_test.dart +++ b/mobile/openapi/test/asset_response_dto_test.dart @@ -52,6 +52,11 @@ void main() { // TODO }); + // bool hasMetadata + test('to test the property `hasMetadata`', () async { + // TODO + }); + // String id test('to test the property `id`', () async { // TODO @@ -142,7 +147,6 @@ void main() { // TODO }); - // base64 encoded thumbhash // String thumbhash test('to test the property `thumbhash`', () async { // TODO diff --git a/mobile/openapi/test/shared_link_create_dto_test.dart b/mobile/openapi/test/shared_link_create_dto_test.dart index 397e6e902b..e02cbe481e 100644 --- a/mobile/openapi/test/shared_link_create_dto_test.dart +++ b/mobile/openapi/test/shared_link_create_dto_test.dart @@ -46,8 +46,8 @@ void main() { // TODO }); - // bool showExif (default value: true) - test('to test the property `showExif`', () async { + // bool showMetadata (default value: true) + test('to test the property `showMetadata`', () async { // TODO }); diff --git a/mobile/openapi/test/shared_link_edit_dto_test.dart b/mobile/openapi/test/shared_link_edit_dto_test.dart index 7df8a5e488..26fbb92fde 100644 --- a/mobile/openapi/test/shared_link_edit_dto_test.dart +++ b/mobile/openapi/test/shared_link_edit_dto_test.dart @@ -36,8 +36,8 @@ void main() { // TODO }); - // bool showExif - test('to test the property `showExif`', () async { + // bool showMetadata + test('to test the property `showMetadata`', () async { // TODO }); diff --git a/mobile/openapi/test/shared_link_response_dto_test.dart b/mobile/openapi/test/shared_link_response_dto_test.dart index 867fb0b133..fbe26b9ae3 100644 --- a/mobile/openapi/test/shared_link_response_dto_test.dart +++ b/mobile/openapi/test/shared_link_response_dto_test.dart @@ -61,8 +61,8 @@ void main() { // TODO }); - // bool showExif - test('to test the property `showExif`', () async { + // bool showMetadata + test('to test the property `showMetadata`', () async { // TODO }); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 19b553b9e9..6392669b15 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -5770,6 +5770,9 @@ "format": "date-time", "type": "string" }, + "hasMetadata": { + "type": "boolean" + }, "id": { "type": "string" }, @@ -5833,7 +5836,6 @@ "type": "array" }, "thumbhash": { - "description": "base64 encoded thumbhash", "nullable": true, "type": "string" }, @@ -5847,7 +5849,6 @@ }, "required": [ "type", - "id", "deviceAssetId", "deviceId", "ownerId", @@ -5855,19 +5856,21 @@ "originalPath", "originalFileName", "resized", - "thumbhash", "fileCreatedAt", "fileModifiedAt", "updatedAt", "isFavorite", "isArchived", "isTrashed", - "localDateTime", "isOffline", "isExternal", "isReadOnly", + "checksum", + "id", + "thumbhash", + "localDateTime", "duration", - "checksum" + "hasMetadata" ], "type": "object" }, @@ -7599,7 +7602,7 @@ "nullable": true, "type": "string" }, - "showExif": { + "showMetadata": { "default": true, "type": "boolean" }, @@ -7628,7 +7631,7 @@ "nullable": true, "type": "string" }, - "showExif": { + "showMetadata": { "type": "boolean" } }, @@ -7670,7 +7673,7 @@ "key": { "type": "string" }, - "showExif": { + "showMetadata": { "type": "boolean" }, "type": { @@ -7691,7 +7694,7 @@ "assets", "allowUpload", "allowDownload", - "showExif" + "showMetadata" ], "type": "object" }, diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 27abedd1e2..3f7a9e33ab 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -47,6 +47,7 @@ import { BulkIdsDto, MapMarkerResponseDto, MemoryLaneResponseDto, + SanitizedAssetResponseDto, TimeBucketResponseDto, mapAsset, } from './response-dto'; @@ -198,10 +199,17 @@ export class AssetService { return this.assetRepository.getTimeBuckets(dto); } - async getByTimeBucket(authUser: AuthUserDto, dto: TimeBucketAssetDto): Promise { + async getByTimeBucket( + authUser: AuthUserDto, + dto: TimeBucketAssetDto, + ): Promise { await this.timeBucketChecks(authUser, 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 { diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index 53454056ab..e7d5061be1 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -6,43 +6,62 @@ import { UserResponseDto, mapUser } from '../../user/response-dto/user-response. import { ExifResponseDto, mapExif } from './exif-response.dto'; import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto'; -export class AssetResponseDto { +export class SanitizedAssetResponseDto { 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; deviceId!: string; ownerId!: string; owner?: UserResponseDto; libraryId!: string; - - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) - type!: AssetType; originalPath!: string; originalFileName!: string; resized!: boolean; - /**base64 encoded thumbhash */ - thumbhash!: string | null; fileCreatedAt!: Date; fileModifiedAt!: Date; updatedAt!: Date; isFavorite!: boolean; isArchived!: boolean; isTrashed!: boolean; - localDateTime!: Date; isOffline!: boolean; isExternal!: boolean; isReadOnly!: boolean; - duration!: string; exifInfo?: ExifResponseDto; smartInfo?: SmartInfoResponseDto; - livePhotoVideoId?: string | null; tags?: TagResponseDto[]; people?: PersonResponseDto[]; /**base64 encoded sha1 hash */ 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 { + ...sanitizedAssetResponse, id: entity.id, deviceAssetId: entity.deviceAssetId, ownerId: entity.ownerId, @@ -62,7 +81,7 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto { isArchived: entity.isArchived, isTrashed: !!entity.deletedAt, 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, livePhotoVideoId: entity.livePhotoVideoId, tags: entity.tags?.map(mapTag), @@ -71,17 +90,10 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto { isExternal: entity.isExternal, isOffline: entity.isOffline, 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 { title!: string; assets!: AssetResponseDto[]; diff --git a/server/src/domain/asset/response-dto/exif-response.dto.ts b/server/src/domain/asset/response-dto/exif-response.dto.ts index 8c9e5843c2..cb0f8399a1 100644 --- a/server/src/domain/asset/response-dto/exif-response.dto.ts +++ b/server/src/domain/asset/response-dto/exif-response.dto.ts @@ -52,3 +52,15 @@ export function mapExif(entity: ExifEntity): ExifResponseDto { 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, + }; +} diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index 141abb0b7c..01cb5adac4 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -380,7 +380,7 @@ export class AuthService { sharedLinkId: link.id, isAllowUpload: link.allowUpload, isAllowDownload: link.allowDownload, - isShowExif: link.showExif, + isShowMetadata: link.showExif, }; } } @@ -431,7 +431,7 @@ export class AuthService { isPublicUser: false, isAllowUpload: true, isAllowDownload: true, - isShowExif: true, + isShowMetadata: true, accessTokenId: token.id, }; } diff --git a/server/src/domain/auth/dto/auth-user.dto.ts b/server/src/domain/auth/dto/auth-user.dto.ts index 0f2c9e41d3..a689096d84 100644 --- a/server/src/domain/auth/dto/auth-user.dto.ts +++ b/server/src/domain/auth/dto/auth-user.dto.ts @@ -6,7 +6,7 @@ export class AuthUserDto { sharedLinkId?: string; isAllowUpload?: boolean; isAllowDownload?: boolean; - isShowExif?: boolean; + isShowMetadata?: boolean; accessTokenId?: string; externalPath?: string | null; } diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index b806862df2..162ab8fdb4 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -97,7 +97,7 @@ export class PersonService { async getAssets(authUser: AuthUserDto, id: string): Promise { await this.access.requirePermission(authUser, Permission.PERSON_READ, 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 { diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 5100e1b4ac..ba637bb3bb 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -154,7 +154,7 @@ export class SearchService { items: assets.items .map((item) => lookup[item.id]) .filter((item) => !!item) - .map(mapAsset), + .map((asset) => mapAsset(asset)), }, }; } diff --git a/server/src/domain/shared-link/shared-link-response.dto.ts b/server/src/domain/shared-link/shared-link-response.dto.ts index f3b0d16784..52592d36fa 100644 --- a/server/src/domain/shared-link/shared-link-response.dto.ts +++ b/server/src/domain/shared-link/shared-link-response.dto.ts @@ -2,7 +2,7 @@ import { SharedLinkEntity, SharedLinkType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import _ from 'lodash'; import { AlbumResponseDto, mapAlbumWithoutAssets } from '../album'; -import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../asset'; +import { AssetResponseDto, mapAsset } from '../asset'; export class SharedLinkResponseDto { id!: string; @@ -17,8 +17,9 @@ export class SharedLinkResponseDto { assets!: AssetResponseDto[]; album?: AlbumResponseDto; allowUpload!: boolean; + allowDownload!: boolean; - showExif!: boolean; + showMetadata!: boolean; } export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto { @@ -35,15 +36,15 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD type: sharedLink.type, createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, - assets: assets.map(mapAsset), + assets: assets.map((asset) => mapAsset(asset)), album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, 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 albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset); @@ -57,10 +58,10 @@ export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLin type: sharedLink.type, createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, - assets: assets.map(mapAssetWithoutExif), + assets: assets.map((asset) => mapAsset(asset, true)) as AssetResponseDto[], album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, allowDownload: sharedLink.allowDownload, - showExif: sharedLink.showExif, + showMetadata: sharedLink.showExif, }; } diff --git a/server/src/domain/shared-link/shared-link.dto.ts b/server/src/domain/shared-link/shared-link.dto.ts index 0ea5b70c64..4c86afb628 100644 --- a/server/src/domain/shared-link/shared-link.dto.ts +++ b/server/src/domain/shared-link/shared-link.dto.ts @@ -34,7 +34,7 @@ export class SharedLinkCreateDto { @Optional() @IsBoolean() - showExif?: boolean = true; + showMetadata?: boolean = true; } export class SharedLinkEditDto { @@ -51,5 +51,5 @@ export class SharedLinkEditDto { allowDownload?: boolean; @Optional() - showExif?: boolean; + showMetadata?: boolean; } diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts index ae3a5a374a..f902d7a68a 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/domain/shared-link/shared-link.service.spec.ts @@ -59,10 +59,10 @@ describe(SharedLinkService.name, () => { 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; 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); }); }); @@ -137,7 +137,7 @@ describe(SharedLinkService.name, () => { await sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: [assetStub.image.id], - showExif: true, + showMetadata: true, allowDownload: true, allowUpload: true, }); diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts index a3c19b8c40..06b5b78978 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/domain/shared-link/shared-link.service.ts @@ -4,7 +4,7 @@ import { AccessCore, Permission } from '../access'; import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset'; import { AuthUserDto } from '../auth'; 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'; @Injectable() @@ -24,7 +24,7 @@ export class SharedLinkService { } async getMine(authUser: AuthUserDto): Promise { - const { sharedLinkId: id, isPublicUser, isShowExif } = authUser; + const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser; if (!isPublicUser || !id) { throw new ForbiddenException(); @@ -69,7 +69,7 @@ export class SharedLinkService { expiresAt: dto.expiresAt || null, allowUpload: dto.allowUpload ?? true, allowDownload: dto.allowDownload ?? true, - showExif: dto.showExif ?? true, + showExif: dto.showMetadata ?? true, }); return this.map(sharedLink, { withExif: true }); @@ -84,7 +84,7 @@ export class SharedLinkService { expiresAt: dto.expiresAt, allowUpload: dto.allowUpload, allowDownload: dto.allowDownload, - showExif: dto.showExif, + showExif: dto.showMetadata, }); return this.map(sharedLink, { withExif: true }); } @@ -157,6 +157,6 @@ export class SharedLinkService { } private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) { - return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithNoExif(sharedLink); + return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink); } } diff --git a/server/src/domain/tag/tag.service.ts b/server/src/domain/tag/tag.service.ts index cf4bffdaa8..ea6dae9a0d 100644 --- a/server/src/domain/tag/tag.service.ts +++ b/server/src/domain/tag/tag.service.ts @@ -47,7 +47,7 @@ export class TagService { async getAssets(authUser: AuthUserDto, id: string): Promise { await this.findOrFail(authUser, 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 { diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index ad6e160b35..9df52f7104 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -186,7 +186,7 @@ export class AssetController { @SharedLinkRoute() @Get('/assetById/:id') getAssetById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.assetService.getAssetById(authUser, id); + return this.assetService.getAssetById(authUser, id) as Promise; } /** diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 9f2c25196b..d3c1fe8764 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -10,9 +10,9 @@ import { IStorageRepository, JobName, mapAsset, - mapAssetWithoutExif, mimeTypes, Permission, + SanitizedAssetResponseDto, UploadFile, } from '@app/domain'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; @@ -187,22 +187,29 @@ export class AssetService { return assets.map((asset) => mapAsset(asset)); } - public async getAssetById(authUser: AuthUserDto, assetId: string): Promise { + public async getAssetById( + authUser: AuthUserDto, + assetId: string, + ): Promise { 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 data = allowExif ? mapAsset(asset) : mapAssetWithoutExif(asset); + if (includeMetadata) { + const data = mapAsset(asset); - if (data.ownerId !== authUser.id) { - data.people = []; + if (data.ownerId !== authUser.id) { + data.people = []; + } + + if (authUser.isPublicUser) { + delete data.owner; + } + + return data; + } else { + return mapAsset(asset, true); } - - if (authUser.isPublicUser) { - delete data.owner; - } - - return data; } async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) { @@ -374,7 +381,7 @@ export class AssetService { } getExifPermission(authUser: AuthUserDto) { - return !authUser.isPublicUser || authUser.isShowExif; + return !authUser.isPublicUser || authUser.isShowMetadata; } private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index 4780906ada..f4f376e98d 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -98,7 +98,7 @@ export class AssetController { @Authenticated({ isShared: true }) @Get('time-bucket') getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise { - return this.service.getByTimeBucket(authUser, dto); + return this.service.getByTimeBucket(authUser, dto) as Promise; } @Post('jobs') diff --git a/server/test/api/shared-link-api.ts b/server/test/api/shared-link-api.ts index c34093b0ac..d6179f6b6f 100644 --- a/server/test/api/shared-link-api.ts +++ b/server/test/api/shared-link-api.ts @@ -10,4 +10,11 @@ export const sharedLinkApi = { expect(status).toBe(201); 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; + }, }; diff --git a/server/test/assets b/server/test/assets index 9e6e1bcc24..948f353e3c 160000 --- a/server/test/assets +++ b/server/test/assets @@ -1 +1 @@ -Subproject commit 9e6e1bcc245e0ae0285bb596faf310ead851fac6 +Subproject commit 948f353e3c9b66156c86c86cf078e0746ec1598e diff --git a/server/test/e2e/shared-link.e2e-spec.ts b/server/test/e2e/shared-link.e2e-spec.ts index 2f88f7cefd..3a52c15a0f 100644 --- a/server/test/e2e/shared-link.e2e-spec.ts +++ b/server/test/e2e/shared-link.e2e-spec.ts @@ -1,11 +1,17 @@ import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain'; import { PartnerController } from '@app/immich'; -import { SharedLinkType } from '@app/infra/entities'; +import { LibraryType, SharedLinkType } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; 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'; const user1Dto = { @@ -18,24 +24,22 @@ const user1Dto = { describe(`${PartnerController.name} (e2e)`, () => { let app: INestApplication; let server: any; - let loginResponse: LoginResponseDto; - let accessToken: string; + let admin: LoginResponseDto; let user1: LoginResponseDto; let album: AlbumResponseDto; let sharedLink: SharedLinkResponseDto; beforeAll(async () => { - app = await createTestApp(); + app = await createTestApp(true); server = app.getHttpServer(); }); beforeEach(async () => { await db.reset(); await api.authApi.adminSignUp(server); - loginResponse = await api.authApi.adminLogin(server); - accessToken = loginResponse.accessToken; + admin = await api.authApi.adminLogin(server); - 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 }); album = await api.albumApi.create(server, user1.accessToken, { albumName: 'shared with link' }); @@ -48,6 +52,7 @@ describe(`${PartnerController.name} (e2e)`, () => { afterAll(async () => { await db.disconnect(); await app.close(); + await restoreTempFolder(); }); describe('GET /shared-link', () => { @@ -68,7 +73,9 @@ describe(`${PartnerController.name} (e2e)`, () => { }); 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(body).toEqual([]); @@ -77,7 +84,9 @@ describe(`${PartnerController.name} (e2e)`, () => { describe('GET /shared-link/me', () => { 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); }); @@ -104,7 +113,7 @@ describe(`${PartnerController.name} (e2e)`, () => { type: SharedLinkType.ALBUM, 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 }); @@ -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 () => { const { status, body } = await request(server) .get(`/shared-link/${sharedLink.id}`) - .set('Authorization', `Bearer ${accessToken}`); + .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' })); @@ -248,4 +257,81 @@ describe(`${PartnerController.name} (e2e)`, () => { 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'); + }); + }); }); diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 154ba0f676..6a45c16afb 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -48,7 +48,7 @@ export const authStub = { isPublicUser: false, isAllowUpload: true, isAllowDownload: true, - isShowExif: true, + isShowMetadata: true, accessTokenId: 'token-id', externalPath: null, }), @@ -59,7 +59,7 @@ export const authStub = { isPublicUser: false, isAllowUpload: true, isAllowDownload: true, - isShowExif: true, + isShowMetadata: true, accessTokenId: 'token-id', externalPath: null, }), @@ -70,7 +70,7 @@ export const authStub = { isPublicUser: false, isAllowUpload: true, isAllowDownload: true, - isShowExif: true, + isShowMetadata: true, accessTokenId: 'token-id', externalPath: '/data/user1', }), @@ -81,7 +81,7 @@ export const authStub = { isAllowUpload: true, isAllowDownload: true, isPublicUser: true, - isShowExif: true, + isShowMetadata: true, sharedLinkId: '123', }), adminSharedLinkNoExif: Object.freeze({ @@ -91,7 +91,7 @@ export const authStub = { isAllowUpload: true, isAllowDownload: true, isPublicUser: true, - isShowExif: false, + isShowMetadata: false, sharedLinkId: '123', }), readonlySharedLink: Object.freeze({ @@ -101,7 +101,7 @@ export const authStub = { isAllowUpload: false, isAllowDownload: false, isPublicUser: true, - isShowExif: true, + isShowMetadata: true, sharedLinkId: '123', accessTokenId: 'token-id', }), diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index a5d180abe3..acb14c6b2d 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -71,8 +71,20 @@ const assetResponse: AssetResponseDto = { checksum: 'ZmlsZSBoYXNo', isTrashed: false, 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 = { albumName: 'Test Album', description: '', @@ -253,7 +265,7 @@ export const sharedLinkResponseStub = { expiresAt: tomorrow, id: '123', key: sharedLinkBytes.toString('base64url'), - showExif: true, + showMetadata: true, type: SharedLinkType.ALBUM, userId: 'admin_id', }), @@ -267,7 +279,7 @@ export const sharedLinkResponseStub = { expiresAt: yesterday, id: '123', key: sharedLinkBytes.toString('base64url'), - showExif: true, + showMetadata: true, type: SharedLinkType.ALBUM, userId: 'admin_id', }), @@ -281,11 +293,11 @@ export const sharedLinkResponseStub = { description: null, allowUpload: false, allowDownload: false, - showExif: true, + showMetadata: true, album: albumResponse, assets: [assetResponse], }), - readonlyNoExif: Object.freeze({ + readonlyNoMetadata: Object.freeze({ id: '123', userId: 'admin_id', key: sharedLinkBytes.toString('base64url'), @@ -295,8 +307,8 @@ export const sharedLinkResponseStub = { description: null, allowUpload: false, allowDownload: false, - showExif: false, + showMetadata: false, album: { ...albumResponse, startDate: assetResponse.fileCreatedAt, endDate: assetResponse.fileCreatedAt }, - assets: [{ ...assetResponse, exifInfo: undefined }], + assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }], }), }; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 2be639a0f1..0c8d2673cc 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -640,6 +640,12 @@ export interface AssetResponseDto { * @memberof AssetResponseDto */ 'fileModifiedAt': string; + /** + * + * @type {boolean} + * @memberof AssetResponseDto + */ + 'hasMetadata': boolean; /** * * @type {string} @@ -749,7 +755,7 @@ export interface AssetResponseDto { */ 'tags'?: Array; /** - * base64 encoded thumbhash + * * @type {string} * @memberof AssetResponseDto */ @@ -2882,7 +2888,7 @@ export interface SharedLinkCreateDto { * @type {boolean} * @memberof SharedLinkCreateDto */ - 'showExif'?: boolean; + 'showMetadata'?: boolean; /** * * @type {SharedLinkType} @@ -2927,7 +2933,7 @@ export interface SharedLinkEditDto { * @type {boolean} * @memberof SharedLinkEditDto */ - 'showExif'?: boolean; + 'showMetadata'?: boolean; } /** * @@ -2994,7 +3000,7 @@ export interface SharedLinkResponseDto { * @type {boolean} * @memberof SharedLinkResponseDto */ - 'showExif': boolean; + 'showMetadata': boolean; /** * * @type {SharedLinkType} diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 2394964d90..fe83d087b5 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -28,6 +28,7 @@ export let showMotionPlayButton: boolean; export let isMotionPhotoPlaying = false; export let showDownloadButton: boolean; + export let showDetailButton: boolean; export let showSlideshow = false; const isOwner = asset.ownerId === $page.data.user?.id; @@ -133,7 +134,14 @@ title="Download" /> {/if} - dispatch('showDetail')} title="Info" /> + {#if showDetailButton} + dispatch('showDetail')} + title="Info" + /> + {/if} {#if isOwner} handleKeyboardPress(keyInfo); @@ -392,6 +393,7 @@ showZoomButton={asset.type === AssetTypeEnum.Image} showMotionPlayButton={!!asset.livePhotoVideoId} showDownloadButton={shouldShowDownloadButton} + showDetailButton={shouldShowDetailButton} showSlideshow={!!assetStore} on:goBack={closeViewer} on:showDetail={showDetailInfoHandler} @@ -433,9 +435,9 @@ on:close={closeViewer} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} /> - {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || asset.originalPath - .toLowerCase() - .endsWith('.insp')} + {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath + .toLowerCase() + .endsWith('.insp'))} {:else} diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 5ebc4d3e68..1c8506bdf4 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -21,7 +21,7 @@ let description = ''; let allowDownload = true; let allowUpload = false; - let showExif = true; + let showMetadata = true; let expirationTime = ''; let shouldChangeExpirationTime = false; let canCopyImagesToClipboard = true; @@ -41,7 +41,7 @@ } allowUpload = editingLink.allowUpload; allowDownload = editingLink.allowDownload; - showExif = editingLink.showExif; + showMetadata = editingLink.showMetadata; albumId = editingLink.album?.id; assetIds = editingLink.assets.map(({ id }) => id); @@ -66,7 +66,7 @@ allowUpload, description, allowDownload, - showExif, + showMetadata, }, }); sharedLink = `${window.location.origin}/share/${data.key}`; @@ -119,9 +119,9 @@ sharedLinkEditDto: { description, expiresAt: shouldChangeExpirationTime ? expirationDate : undefined, - allowUpload: allowUpload, - allowDownload: allowDownload, - showExif: showExif, + allowUpload, + allowDownload, + showMetadata, }, }); @@ -184,7 +184,7 @@
- +
diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte index 664e6f3154..b8e24ddcd4 100644 --- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte +++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte @@ -136,7 +136,7 @@
{/if} - {#if link.showExif} + {#if link.showMetadata}