1
0
mirror of https://github.com/immich-app/immich.git synced 2025-01-27 17:28:09 +02: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:
Jonathan Jogenfors 2023-10-14 03:46:30 +02:00 committed by GitHub
parent 4a9f58bf9b
commit dadcf49eca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 332 additions and 150 deletions

View File

@ -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}

View File

@ -15,6 +15,7 @@ Name | Type | Description | Notes
**exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) | | [optional] **exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) | | [optional]
**fileCreatedAt** | [**DateTime**](DateTime.md) | | **fileCreatedAt** | [**DateTime**](DateTime.md) | |
**fileModifiedAt** | [**DateTime**](DateTime.md) | | **fileModifiedAt** | [**DateTime**](DateTime.md) | |
**hasMetadata** | **bool** | |
**id** | **String** | | **id** | **String** | |
**isArchived** | **bool** | | **isArchived** | **bool** | |
**isExternal** | **bool** | | **isExternal** | **bool** | |
@ -33,7 +34,7 @@ Name | Type | Description | Notes
**resized** | **bool** | | **resized** | **bool** | |
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional] **smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
**tags** | [**List<TagResponseDto>**](TagResponseDto.md) | | [optional] [default to const []] **tags** | [**List<TagResponseDto>**](TagResponseDto.md) | | [optional] [default to const []]
**thumbhash** | **String** | base64 encoded thumbhash | **thumbhash** | **String** | |
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | **type** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
**updatedAt** | [**DateTime**](DateTime.md) | | **updatedAt** | [**DateTime**](DateTime.md) | |

View File

@ -14,7 +14,7 @@ Name | Type | Description | Notes
**assetIds** | **List<String>** | | [optional] [default to const []] **assetIds** | **List<String>** | | [optional] [default to const []]
**description** | **String** | | [optional] **description** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional] **expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**showExif** | **bool** | | [optional] [default to true] **showMetadata** | **bool** | | [optional] [default to true]
**type** | [**SharedLinkType**](SharedLinkType.md) | | **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) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -12,7 +12,7 @@ Name | Type | Description | Notes
**allowUpload** | **bool** | | [optional] **allowUpload** | **bool** | | [optional]
**description** | **String** | | [optional] **description** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [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) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -17,7 +17,7 @@ Name | Type | Description | Notes
**expiresAt** | [**DateTime**](DateTime.md) | | **expiresAt** | [**DateTime**](DateTime.md) | |
**id** | **String** | | **id** | **String** | |
**key** | **String** | | **key** | **String** | |
**showExif** | **bool** | | **showMetadata** | **bool** | |
**type** | [**SharedLinkType**](SharedLinkType.md) | | **type** | [**SharedLinkType**](SharedLinkType.md) | |
**userId** | **String** | | **userId** | **String** | |

View File

@ -20,6 +20,7 @@ class AssetResponseDto {
this.exifInfo, this.exifInfo,
required this.fileCreatedAt, required this.fileCreatedAt,
required this.fileModifiedAt, required this.fileModifiedAt,
required this.hasMetadata,
required this.id, required this.id,
required this.isArchived, required this.isArchived,
required this.isExternal, required this.isExternal,
@ -64,6 +65,8 @@ class AssetResponseDto {
DateTime fileModifiedAt; DateTime fileModifiedAt;
bool hasMetadata;
String id; String id;
bool isArchived; bool isArchived;
@ -112,7 +115,6 @@ class AssetResponseDto {
List<TagResponseDto> tags; List<TagResponseDto> tags;
/// base64 encoded thumbhash
String? thumbhash; String? thumbhash;
AssetTypeEnum type; AssetTypeEnum type;
@ -128,6 +130,7 @@ class AssetResponseDto {
other.exifInfo == exifInfo && other.exifInfo == exifInfo &&
other.fileCreatedAt == fileCreatedAt && other.fileCreatedAt == fileCreatedAt &&
other.fileModifiedAt == fileModifiedAt && other.fileModifiedAt == fileModifiedAt &&
other.hasMetadata == hasMetadata &&
other.id == id && other.id == id &&
other.isArchived == isArchived && other.isArchived == isArchived &&
other.isExternal == isExternal && other.isExternal == isExternal &&
@ -160,6 +163,7 @@ class AssetResponseDto {
(exifInfo == null ? 0 : exifInfo!.hashCode) + (exifInfo == null ? 0 : exifInfo!.hashCode) +
(fileCreatedAt.hashCode) + (fileCreatedAt.hashCode) +
(fileModifiedAt.hashCode) + (fileModifiedAt.hashCode) +
(hasMetadata.hashCode) +
(id.hashCode) + (id.hashCode) +
(isArchived.hashCode) + (isArchived.hashCode) +
(isExternal.hashCode) + (isExternal.hashCode) +
@ -183,7 +187,7 @@ class AssetResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @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<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -198,6 +202,7 @@ class AssetResponseDto {
} }
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
json[r'hasMetadata'] = this.hasMetadata;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isArchived'] = this.isArchived; json[r'isArchived'] = this.isArchived;
json[r'isExternal'] = this.isExternal; json[r'isExternal'] = this.isExternal;
@ -253,6 +258,7 @@ class AssetResponseDto {
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', '')!, fileCreatedAt: mapDateTime(json, r'fileCreatedAt', '')!,
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', '')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', '')!,
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isArchived: mapValueOfType<bool>(json, r'isArchived')!, isArchived: mapValueOfType<bool>(json, r'isArchived')!,
isExternal: mapValueOfType<bool>(json, r'isExternal')!, isExternal: mapValueOfType<bool>(json, r'isExternal')!,
@ -327,6 +333,7 @@ class AssetResponseDto {
'duration', 'duration',
'fileCreatedAt', 'fileCreatedAt',
'fileModifiedAt', 'fileModifiedAt',
'hasMetadata',
'id', 'id',
'isArchived', 'isArchived',
'isExternal', 'isExternal',

View File

@ -19,7 +19,7 @@ class SharedLinkCreateDto {
this.assetIds = const [], this.assetIds = const [],
this.description, this.description,
this.expiresAt, this.expiresAt,
this.showExif = true, this.showMetadata = true,
required this.type, required this.type,
}); });
@ -47,7 +47,7 @@ class SharedLinkCreateDto {
DateTime? expiresAt; DateTime? expiresAt;
bool showExif; bool showMetadata;
SharedLinkType type; SharedLinkType type;
@ -59,7 +59,7 @@ class SharedLinkCreateDto {
other.assetIds == assetIds && other.assetIds == assetIds &&
other.description == description && other.description == description &&
other.expiresAt == expiresAt && other.expiresAt == expiresAt &&
other.showExif == showExif && other.showMetadata == showMetadata &&
other.type == type; other.type == type;
@override @override
@ -71,11 +71,11 @@ class SharedLinkCreateDto {
(assetIds.hashCode) + (assetIds.hashCode) +
(description == null ? 0 : description!.hashCode) + (description == null ? 0 : description!.hashCode) +
(expiresAt == null ? 0 : expiresAt!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) +
(showExif.hashCode) + (showMetadata.hashCode) +
(type.hashCode); (type.hashCode);
@override @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<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -97,7 +97,7 @@ class SharedLinkCreateDto {
} else { } else {
// json[r'expiresAt'] = null; // json[r'expiresAt'] = null;
} }
json[r'showExif'] = this.showExif; json[r'showMetadata'] = this.showMetadata;
json[r'type'] = this.type; json[r'type'] = this.type;
return json; return json;
} }
@ -118,7 +118,7 @@ class SharedLinkCreateDto {
: const [], : const [],
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
expiresAt: mapDateTime(json, r'expiresAt', ''), expiresAt: mapDateTime(json, r'expiresAt', ''),
showExif: mapValueOfType<bool>(json, r'showExif') ?? true, showMetadata: mapValueOfType<bool>(json, r'showMetadata') ?? true,
type: SharedLinkType.fromJson(json[r'type'])!, type: SharedLinkType.fromJson(json[r'type'])!,
); );
} }

View File

@ -17,7 +17,7 @@ class SharedLinkEditDto {
this.allowUpload, this.allowUpload,
this.description, this.description,
this.expiresAt, this.expiresAt,
this.showExif, this.showMetadata,
}); });
/// ///
@ -52,7 +52,7 @@ class SharedLinkEditDto {
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
bool? showExif; bool? showMetadata;
@override @override
bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto && bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto &&
@ -60,7 +60,7 @@ class SharedLinkEditDto {
other.allowUpload == allowUpload && other.allowUpload == allowUpload &&
other.description == description && other.description == description &&
other.expiresAt == expiresAt && other.expiresAt == expiresAt &&
other.showExif == showExif; other.showMetadata == showMetadata;
@override @override
int get hashCode => int get hashCode =>
@ -69,10 +69,10 @@ class SharedLinkEditDto {
(allowUpload == null ? 0 : allowUpload!.hashCode) + (allowUpload == null ? 0 : allowUpload!.hashCode) +
(description == null ? 0 : description!.hashCode) + (description == null ? 0 : description!.hashCode) +
(expiresAt == null ? 0 : expiresAt!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) +
(showExif == null ? 0 : showExif!.hashCode); (showMetadata == null ? 0 : showMetadata!.hashCode);
@override @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<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -96,10 +96,10 @@ class SharedLinkEditDto {
} else { } else {
// json[r'expiresAt'] = null; // json[r'expiresAt'] = null;
} }
if (this.showExif != null) { if (this.showMetadata != null) {
json[r'showExif'] = this.showExif; json[r'showMetadata'] = this.showMetadata;
} else { } else {
// json[r'showExif'] = null; // json[r'showMetadata'] = null;
} }
return json; return json;
} }
@ -116,7 +116,7 @@ class SharedLinkEditDto {
allowUpload: mapValueOfType<bool>(json, r'allowUpload'), allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
expiresAt: mapDateTime(json, r'expiresAt', ''), expiresAt: mapDateTime(json, r'expiresAt', ''),
showExif: mapValueOfType<bool>(json, r'showExif'), showMetadata: mapValueOfType<bool>(json, r'showMetadata'),
); );
} }
return null; return null;

View File

@ -22,7 +22,7 @@ class SharedLinkResponseDto {
required this.expiresAt, required this.expiresAt,
required this.id, required this.id,
required this.key, required this.key,
required this.showExif, required this.showMetadata,
required this.type, required this.type,
required this.userId, required this.userId,
}); });
@ -51,7 +51,7 @@ class SharedLinkResponseDto {
String key; String key;
bool showExif; bool showMetadata;
SharedLinkType type; SharedLinkType type;
@ -68,7 +68,7 @@ class SharedLinkResponseDto {
other.expiresAt == expiresAt && other.expiresAt == expiresAt &&
other.id == id && other.id == id &&
other.key == key && other.key == key &&
other.showExif == showExif && other.showMetadata == showMetadata &&
other.type == type && other.type == type &&
other.userId == userId; other.userId == userId;
@ -84,12 +84,12 @@ class SharedLinkResponseDto {
(expiresAt == null ? 0 : expiresAt!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) +
(id.hashCode) + (id.hashCode) +
(key.hashCode) + (key.hashCode) +
(showExif.hashCode) + (showMetadata.hashCode) +
(type.hashCode) + (type.hashCode) +
(userId.hashCode); (userId.hashCode);
@override @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<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -114,7 +114,7 @@ class SharedLinkResponseDto {
} }
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'key'] = this.key; json[r'key'] = this.key;
json[r'showExif'] = this.showExif; json[r'showMetadata'] = this.showMetadata;
json[r'type'] = this.type; json[r'type'] = this.type;
json[r'userId'] = this.userId; json[r'userId'] = this.userId;
return json; return json;
@ -137,7 +137,7 @@ class SharedLinkResponseDto {
expiresAt: mapDateTime(json, r'expiresAt', ''), expiresAt: mapDateTime(json, r'expiresAt', ''),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
key: mapValueOfType<String>(json, r'key')!, key: mapValueOfType<String>(json, r'key')!,
showExif: mapValueOfType<bool>(json, r'showExif')!, showMetadata: mapValueOfType<bool>(json, r'showMetadata')!,
type: SharedLinkType.fromJson(json[r'type'])!, type: SharedLinkType.fromJson(json[r'type'])!,
userId: mapValueOfType<String>(json, r'userId')!, userId: mapValueOfType<String>(json, r'userId')!,
); );
@ -195,7 +195,7 @@ class SharedLinkResponseDto {
'expiresAt', 'expiresAt',
'id', 'id',
'key', 'key',
'showExif', 'showMetadata',
'type', 'type',
'userId', 'userId',
}; };

View File

@ -52,6 +52,11 @@ void main() {
// TODO // TODO
}); });
// bool hasMetadata
test('to test the property `hasMetadata`', () async {
// TODO
});
// String id // String id
test('to test the property `id`', () async { test('to test the property `id`', () async {
// TODO // TODO
@ -142,7 +147,6 @@ void main() {
// TODO // TODO
}); });
// base64 encoded thumbhash
// String thumbhash // String thumbhash
test('to test the property `thumbhash`', () async { test('to test the property `thumbhash`', () async {
// TODO // TODO

View File

@ -46,8 +46,8 @@ void main() {
// TODO // TODO
}); });
// bool showExif (default value: true) // bool showMetadata (default value: true)
test('to test the property `showExif`', () async { test('to test the property `showMetadata`', () async {
// TODO // TODO
}); });

View File

@ -36,8 +36,8 @@ void main() {
// TODO // TODO
}); });
// bool showExif // bool showMetadata
test('to test the property `showExif`', () async { test('to test the property `showMetadata`', () async {
// TODO // TODO
}); });

View File

@ -61,8 +61,8 @@ void main() {
// TODO // TODO
}); });
// bool showExif // bool showMetadata
test('to test the property `showExif`', () async { test('to test the property `showMetadata`', () async {
// TODO // TODO
}); });

View File

@ -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"
}, },

View File

@ -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> {

View File

@ -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[];

View File

@ -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,
};
}

View File

@ -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,
}; };
} }

View File

@ -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;
} }

View File

@ -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> {

View File

@ -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)),
}, },
}; };
} }

View File

@ -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,
}; };
} }

View File

@ -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;
} }

View File

@ -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,
}); });

View File

@ -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);
} }
} }

View File

@ -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[]> {

View File

@ -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>;
} }
/** /**

View File

@ -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) {

View File

@ -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')

View File

@ -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

View File

@ -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');
});
});
}); });

View File

@ -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',
}), }),

View File

@ -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 }],
}), }),
}; };

View File

@ -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}

View File

@ -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}

View File

@ -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} />

View File

@ -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">

View File

@ -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"
> >