mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:27:08 -04:00 
			
		
		
		
	feat(web/server) Add more options to public shared link (#1348)
* Added migration files * Added logic for shared album level * Added permission for EXIF * Update shared link response dto * Added condition to show download button * Create and edit link with new parameter: * Remove deadcode * PR feedback * More refactor * Move logic of allow original file to service * Simplify * Wording
This commit is contained in:
		
							parent
							
								
									4cfac47674
								
							
						
					
					
						commit
						b07891089f
					
				| @ -99,7 +99,7 @@ After making any changes in the `server/libs/database/src/entities`, a database | |||||||
| 2. Run | 2. Run | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| npm run typeorm -- migration:generate ./libs/database/src/<migration-name> -d libs/database/src/config/database.config.ts | npm run typeorm -- migration:generate ./libs/infra/src/db/<migration-name> -d ./libs/infra/src/db/config/database.config.ts | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| 3. Check if the migration file makes sense. | 3. Check if the migration file makes sense. | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/CreateAlbumShareLinkDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/CreateAlbumShareLinkDto.md
									
									
									
										generated
									
									
									
								
							| @ -11,6 +11,8 @@ Name | Type | Description | Notes | |||||||
| **albumId** | **String** |  |  | **albumId** | **String** |  |  | ||||||
| **expiredAt** | **String** |  | [optional]  | **expiredAt** | **String** |  | [optional]  | ||||||
| **allowUpload** | **bool** |  | [optional]  | **allowUpload** | **bool** |  | [optional]  | ||||||
|  | **allowDownload** | **bool** |  | [optional]  | ||||||
|  | **showExif** | **bool** |  | [optional]  | ||||||
| **description** | **String** |  | [optional]  | **description** | **String** |  | [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) | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/CreateAssetsShareLinkDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/CreateAssetsShareLinkDto.md
									
									
									
										generated
									
									
									
								
							| @ -11,6 +11,8 @@ Name | Type | Description | Notes | |||||||
| **assetIds** | **List<String>** |  | [default to const []] | **assetIds** | **List<String>** |  | [default to const []] | ||||||
| **expiredAt** | **String** |  | [optional]  | **expiredAt** | **String** |  | [optional]  | ||||||
| **allowUpload** | **bool** |  | [optional]  | **allowUpload** | **bool** |  | [optional]  | ||||||
|  | **allowDownload** | **bool** |  | [optional]  | ||||||
|  | **showExif** | **bool** |  | [optional]  | ||||||
| **description** | **String** |  | [optional]  | **description** | **String** |  | [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) | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/EditSharedLinkDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/EditSharedLinkDto.md
									
									
									
										generated
									
									
									
								
							| @ -11,6 +11,8 @@ Name | Type | Description | Notes | |||||||
| **description** | **String** |  | [optional]  | **description** | **String** |  | [optional]  | ||||||
| **expiredAt** | **String** |  | [optional]  | **expiredAt** | **String** |  | [optional]  | ||||||
| **allowUpload** | **bool** |  | [optional]  | **allowUpload** | **bool** |  | [optional]  | ||||||
|  | **allowDownload** | **bool** |  | [optional]  | ||||||
|  | **showExif** | **bool** |  | [optional]  | ||||||
| **isEditExpireTime** | **bool** |  | [optional]  | **isEditExpireTime** | **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) | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/SharedLinkResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/SharedLinkResponseDto.md
									
									
									
										generated
									
									
									
								
							| @ -18,6 +18,8 @@ Name | Type | Description | Notes | |||||||
| **assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) |  | [default to const []] | **assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) |  | [default to const []] | ||||||
| **album** | [**AlbumResponseDto**](AlbumResponseDto.md) |  | [optional]  | **album** | [**AlbumResponseDto**](AlbumResponseDto.md) |  | [optional]  | ||||||
| **allowUpload** | **bool** |  |  | **allowUpload** | **bool** |  |  | ||||||
|  | **allowDownload** | **bool** |  |  | ||||||
|  | **showExif** | **bool** |  |  | ||||||
| 
 | 
 | ||||||
| [[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) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,6 +16,8 @@ class CreateAlbumShareLinkDto { | |||||||
|     required this.albumId, |     required this.albumId, | ||||||
|     this.expiredAt, |     this.expiredAt, | ||||||
|     this.allowUpload, |     this.allowUpload, | ||||||
|  |     this.allowDownload, | ||||||
|  |     this.showExif, | ||||||
|     this.description, |     this.description, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -37,6 +39,22 @@ class CreateAlbumShareLinkDto { | |||||||
|   /// |   /// | ||||||
|   bool? allowUpload; |   bool? allowUpload; | ||||||
| 
 | 
 | ||||||
|  |   /// | ||||||
|  |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|  |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
|  |   /// source code must fall back to having a nullable type. | ||||||
|  |   /// Consider adding a "default:" property in the specification file to hide this note. | ||||||
|  |   /// | ||||||
|  |   bool? allowDownload; | ||||||
|  | 
 | ||||||
|  |   /// | ||||||
|  |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|  |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
|  |   /// 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; | ||||||
|  | 
 | ||||||
|   /// |   /// | ||||||
|   /// Please note: This property should have been non-nullable! Since the specification file |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|   /// does not include a default value (using the "default:" property), however, the generated |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
| @ -50,6 +68,8 @@ class CreateAlbumShareLinkDto { | |||||||
|      other.albumId == albumId && |      other.albumId == albumId && | ||||||
|      other.expiredAt == expiredAt && |      other.expiredAt == expiredAt && | ||||||
|      other.allowUpload == allowUpload && |      other.allowUpload == allowUpload && | ||||||
|  |      other.allowDownload == allowDownload && | ||||||
|  |      other.showExif == showExif && | ||||||
|      other.description == description; |      other.description == description; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
| @ -58,10 +78,12 @@ class CreateAlbumShareLinkDto { | |||||||
|     (albumId.hashCode) + |     (albumId.hashCode) + | ||||||
|     (expiredAt == null ? 0 : expiredAt!.hashCode) + |     (expiredAt == null ? 0 : expiredAt!.hashCode) + | ||||||
|     (allowUpload == null ? 0 : allowUpload!.hashCode) + |     (allowUpload == null ? 0 : allowUpload!.hashCode) + | ||||||
|  |     (allowDownload == null ? 0 : allowDownload!.hashCode) + | ||||||
|  |     (showExif == null ? 0 : showExif!.hashCode) + | ||||||
|     (description == null ? 0 : description!.hashCode); |     (description == null ? 0 : description!.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'CreateAlbumShareLinkDto[albumId=$albumId, expiredAt=$expiredAt, allowUpload=$allowUpload, description=$description]'; |   String toString() => 'CreateAlbumShareLinkDto[albumId=$albumId, expiredAt=$expiredAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif, description=$description]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @ -76,6 +98,16 @@ class CreateAlbumShareLinkDto { | |||||||
|     } else { |     } else { | ||||||
|       // json[r'allowUpload'] = null; |       // json[r'allowUpload'] = null; | ||||||
|     } |     } | ||||||
|  |     if (this.allowDownload != null) { | ||||||
|  |       json[r'allowDownload'] = this.allowDownload; | ||||||
|  |     } else { | ||||||
|  |       // json[r'allowDownload'] = null; | ||||||
|  |     } | ||||||
|  |     if (this.showExif != null) { | ||||||
|  |       json[r'showExif'] = this.showExif; | ||||||
|  |     } else { | ||||||
|  |       // json[r'showExif'] = null; | ||||||
|  |     } | ||||||
|     if (this.description != null) { |     if (this.description != null) { | ||||||
|       json[r'description'] = this.description; |       json[r'description'] = this.description; | ||||||
|     } else { |     } else { | ||||||
| @ -106,6 +138,8 @@ class CreateAlbumShareLinkDto { | |||||||
|         albumId: mapValueOfType<String>(json, r'albumId')!, |         albumId: mapValueOfType<String>(json, r'albumId')!, | ||||||
|         expiredAt: mapValueOfType<String>(json, r'expiredAt'), |         expiredAt: mapValueOfType<String>(json, r'expiredAt'), | ||||||
|         allowUpload: mapValueOfType<bool>(json, r'allowUpload'), |         allowUpload: mapValueOfType<bool>(json, r'allowUpload'), | ||||||
|  |         allowDownload: mapValueOfType<bool>(json, r'allowDownload'), | ||||||
|  |         showExif: mapValueOfType<bool>(json, r'showExif'), | ||||||
|         description: mapValueOfType<String>(json, r'description'), |         description: mapValueOfType<String>(json, r'description'), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -16,6 +16,8 @@ class CreateAssetsShareLinkDto { | |||||||
|     this.assetIds = const [], |     this.assetIds = const [], | ||||||
|     this.expiredAt, |     this.expiredAt, | ||||||
|     this.allowUpload, |     this.allowUpload, | ||||||
|  |     this.allowDownload, | ||||||
|  |     this.showExif, | ||||||
|     this.description, |     this.description, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -37,6 +39,22 @@ class CreateAssetsShareLinkDto { | |||||||
|   /// |   /// | ||||||
|   bool? allowUpload; |   bool? allowUpload; | ||||||
| 
 | 
 | ||||||
|  |   /// | ||||||
|  |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|  |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
|  |   /// source code must fall back to having a nullable type. | ||||||
|  |   /// Consider adding a "default:" property in the specification file to hide this note. | ||||||
|  |   /// | ||||||
|  |   bool? allowDownload; | ||||||
|  | 
 | ||||||
|  |   /// | ||||||
|  |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|  |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
|  |   /// 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; | ||||||
|  | 
 | ||||||
|   /// |   /// | ||||||
|   /// Please note: This property should have been non-nullable! Since the specification file |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|   /// does not include a default value (using the "default:" property), however, the generated |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
| @ -50,6 +68,8 @@ class CreateAssetsShareLinkDto { | |||||||
|      other.assetIds == assetIds && |      other.assetIds == assetIds && | ||||||
|      other.expiredAt == expiredAt && |      other.expiredAt == expiredAt && | ||||||
|      other.allowUpload == allowUpload && |      other.allowUpload == allowUpload && | ||||||
|  |      other.allowDownload == allowDownload && | ||||||
|  |      other.showExif == showExif && | ||||||
|      other.description == description; |      other.description == description; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
| @ -58,10 +78,12 @@ class CreateAssetsShareLinkDto { | |||||||
|     (assetIds.hashCode) + |     (assetIds.hashCode) + | ||||||
|     (expiredAt == null ? 0 : expiredAt!.hashCode) + |     (expiredAt == null ? 0 : expiredAt!.hashCode) + | ||||||
|     (allowUpload == null ? 0 : allowUpload!.hashCode) + |     (allowUpload == null ? 0 : allowUpload!.hashCode) + | ||||||
|  |     (allowDownload == null ? 0 : allowDownload!.hashCode) + | ||||||
|  |     (showExif == null ? 0 : showExif!.hashCode) + | ||||||
|     (description == null ? 0 : description!.hashCode); |     (description == null ? 0 : description!.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'CreateAssetsShareLinkDto[assetIds=$assetIds, expiredAt=$expiredAt, allowUpload=$allowUpload, description=$description]'; |   String toString() => 'CreateAssetsShareLinkDto[assetIds=$assetIds, expiredAt=$expiredAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif, description=$description]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @ -76,6 +98,16 @@ class CreateAssetsShareLinkDto { | |||||||
|     } else { |     } else { | ||||||
|       // json[r'allowUpload'] = null; |       // json[r'allowUpload'] = null; | ||||||
|     } |     } | ||||||
|  |     if (this.allowDownload != null) { | ||||||
|  |       json[r'allowDownload'] = this.allowDownload; | ||||||
|  |     } else { | ||||||
|  |       // json[r'allowDownload'] = null; | ||||||
|  |     } | ||||||
|  |     if (this.showExif != null) { | ||||||
|  |       json[r'showExif'] = this.showExif; | ||||||
|  |     } else { | ||||||
|  |       // json[r'showExif'] = null; | ||||||
|  |     } | ||||||
|     if (this.description != null) { |     if (this.description != null) { | ||||||
|       json[r'description'] = this.description; |       json[r'description'] = this.description; | ||||||
|     } else { |     } else { | ||||||
| @ -108,6 +140,8 @@ class CreateAssetsShareLinkDto { | |||||||
|             : const [], |             : const [], | ||||||
|         expiredAt: mapValueOfType<String>(json, r'expiredAt'), |         expiredAt: mapValueOfType<String>(json, r'expiredAt'), | ||||||
|         allowUpload: mapValueOfType<bool>(json, r'allowUpload'), |         allowUpload: mapValueOfType<bool>(json, r'allowUpload'), | ||||||
|  |         allowDownload: mapValueOfType<bool>(json, r'allowDownload'), | ||||||
|  |         showExif: mapValueOfType<bool>(json, r'showExif'), | ||||||
|         description: mapValueOfType<String>(json, r'description'), |         description: mapValueOfType<String>(json, r'description'), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										36
									
								
								mobile/openapi/lib/model/edit_shared_link_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										36
									
								
								mobile/openapi/lib/model/edit_shared_link_dto.dart
									
									
									
										generated
									
									
									
								
							| @ -16,6 +16,8 @@ class EditSharedLinkDto { | |||||||
|     this.description, |     this.description, | ||||||
|     this.expiredAt, |     this.expiredAt, | ||||||
|     this.allowUpload, |     this.allowUpload, | ||||||
|  |     this.allowDownload, | ||||||
|  |     this.showExif, | ||||||
|     this.isEditExpireTime, |     this.isEditExpireTime, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -43,6 +45,22 @@ class EditSharedLinkDto { | |||||||
|   /// |   /// | ||||||
|   bool? allowUpload; |   bool? allowUpload; | ||||||
| 
 | 
 | ||||||
|  |   /// | ||||||
|  |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|  |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
|  |   /// source code must fall back to having a nullable type. | ||||||
|  |   /// Consider adding a "default:" property in the specification file to hide this note. | ||||||
|  |   /// | ||||||
|  |   bool? allowDownload; | ||||||
|  | 
 | ||||||
|  |   /// | ||||||
|  |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|  |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
|  |   /// 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; | ||||||
|  | 
 | ||||||
|   /// |   /// | ||||||
|   /// Please note: This property should have been non-nullable! Since the specification file |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|   /// does not include a default value (using the "default:" property), however, the generated |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
| @ -56,6 +74,8 @@ class EditSharedLinkDto { | |||||||
|      other.description == description && |      other.description == description && | ||||||
|      other.expiredAt == expiredAt && |      other.expiredAt == expiredAt && | ||||||
|      other.allowUpload == allowUpload && |      other.allowUpload == allowUpload && | ||||||
|  |      other.allowDownload == allowDownload && | ||||||
|  |      other.showExif == showExif && | ||||||
|      other.isEditExpireTime == isEditExpireTime; |      other.isEditExpireTime == isEditExpireTime; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
| @ -64,10 +84,12 @@ class EditSharedLinkDto { | |||||||
|     (description == null ? 0 : description!.hashCode) + |     (description == null ? 0 : description!.hashCode) + | ||||||
|     (expiredAt == null ? 0 : expiredAt!.hashCode) + |     (expiredAt == null ? 0 : expiredAt!.hashCode) + | ||||||
|     (allowUpload == null ? 0 : allowUpload!.hashCode) + |     (allowUpload == null ? 0 : allowUpload!.hashCode) + | ||||||
|  |     (allowDownload == null ? 0 : allowDownload!.hashCode) + | ||||||
|  |     (showExif == null ? 0 : showExif!.hashCode) + | ||||||
|     (isEditExpireTime == null ? 0 : isEditExpireTime!.hashCode); |     (isEditExpireTime == null ? 0 : isEditExpireTime!.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'EditSharedLinkDto[description=$description, expiredAt=$expiredAt, allowUpload=$allowUpload, isEditExpireTime=$isEditExpireTime]'; |   String toString() => 'EditSharedLinkDto[description=$description, expiredAt=$expiredAt, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif, isEditExpireTime=$isEditExpireTime]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @ -86,6 +108,16 @@ class EditSharedLinkDto { | |||||||
|     } else { |     } else { | ||||||
|       // json[r'allowUpload'] = null; |       // json[r'allowUpload'] = null; | ||||||
|     } |     } | ||||||
|  |     if (this.allowDownload != null) { | ||||||
|  |       json[r'allowDownload'] = this.allowDownload; | ||||||
|  |     } else { | ||||||
|  |       // json[r'allowDownload'] = null; | ||||||
|  |     } | ||||||
|  |     if (this.showExif != null) { | ||||||
|  |       json[r'showExif'] = this.showExif; | ||||||
|  |     } else { | ||||||
|  |       // json[r'showExif'] = null; | ||||||
|  |     } | ||||||
|     if (this.isEditExpireTime != null) { |     if (this.isEditExpireTime != null) { | ||||||
|       json[r'isEditExpireTime'] = this.isEditExpireTime; |       json[r'isEditExpireTime'] = this.isEditExpireTime; | ||||||
|     } else { |     } else { | ||||||
| @ -116,6 +148,8 @@ class EditSharedLinkDto { | |||||||
|         description: mapValueOfType<String>(json, r'description'), |         description: mapValueOfType<String>(json, r'description'), | ||||||
|         expiredAt: mapValueOfType<String>(json, r'expiredAt'), |         expiredAt: mapValueOfType<String>(json, r'expiredAt'), | ||||||
|         allowUpload: mapValueOfType<bool>(json, r'allowUpload'), |         allowUpload: mapValueOfType<bool>(json, r'allowUpload'), | ||||||
|  |         allowDownload: mapValueOfType<bool>(json, r'allowDownload'), | ||||||
|  |         showExif: mapValueOfType<bool>(json, r'showExif'), | ||||||
|         isEditExpireTime: mapValueOfType<bool>(json, r'isEditExpireTime'), |         isEditExpireTime: mapValueOfType<bool>(json, r'isEditExpireTime'), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -23,6 +23,8 @@ class SharedLinkResponseDto { | |||||||
|     this.assets = const [], |     this.assets = const [], | ||||||
|     this.album, |     this.album, | ||||||
|     required this.allowUpload, |     required this.allowUpload, | ||||||
|  |     required this.allowDownload, | ||||||
|  |     required this.showExif, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   SharedLinkType type; |   SharedLinkType type; | ||||||
| @ -57,6 +59,10 @@ class SharedLinkResponseDto { | |||||||
| 
 | 
 | ||||||
|   bool allowUpload; |   bool allowUpload; | ||||||
| 
 | 
 | ||||||
|  |   bool allowDownload; | ||||||
|  | 
 | ||||||
|  |   bool showExif; | ||||||
|  | 
 | ||||||
|   @override |   @override | ||||||
|   bool operator ==(Object other) => identical(this, other) || other is SharedLinkResponseDto && |   bool operator ==(Object other) => identical(this, other) || other is SharedLinkResponseDto && | ||||||
|      other.type == type && |      other.type == type && | ||||||
| @ -68,7 +74,9 @@ class SharedLinkResponseDto { | |||||||
|      other.expiresAt == expiresAt && |      other.expiresAt == expiresAt && | ||||||
|      other.assets == assets && |      other.assets == assets && | ||||||
|      other.album == album && |      other.album == album && | ||||||
|      other.allowUpload == allowUpload; |      other.allowUpload == allowUpload && | ||||||
|  |      other.allowDownload == allowDownload && | ||||||
|  |      other.showExif == showExif; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   int get hashCode => |   int get hashCode => | ||||||
| @ -82,10 +90,12 @@ class SharedLinkResponseDto { | |||||||
|     (expiresAt == null ? 0 : expiresAt!.hashCode) + |     (expiresAt == null ? 0 : expiresAt!.hashCode) + | ||||||
|     (assets.hashCode) + |     (assets.hashCode) + | ||||||
|     (album == null ? 0 : album!.hashCode) + |     (album == null ? 0 : album!.hashCode) + | ||||||
|     (allowUpload.hashCode); |     (allowUpload.hashCode) + | ||||||
|  |     (allowDownload.hashCode) + | ||||||
|  |     (showExif.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SharedLinkResponseDto[type=$type, id=$id, description=$description, userId=$userId, key=$key, createdAt=$createdAt, expiresAt=$expiresAt, assets=$assets, album=$album, allowUpload=$allowUpload]'; |   String toString() => 'SharedLinkResponseDto[type=$type, id=$id, description=$description, userId=$userId, key=$key, createdAt=$createdAt, expiresAt=$expiresAt, assets=$assets, album=$album, allowUpload=$allowUpload, allowDownload=$allowDownload, showExif=$showExif]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @ -111,6 +121,8 @@ class SharedLinkResponseDto { | |||||||
|       // json[r'album'] = null; |       // json[r'album'] = null; | ||||||
|     } |     } | ||||||
|       json[r'allowUpload'] = this.allowUpload; |       json[r'allowUpload'] = this.allowUpload; | ||||||
|  |       json[r'allowDownload'] = this.allowDownload; | ||||||
|  |       json[r'showExif'] = this.showExif; | ||||||
|     return json; |     return json; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -143,6 +155,8 @@ class SharedLinkResponseDto { | |||||||
|         assets: AssetResponseDto.listFromJson(json[r'assets'])!, |         assets: AssetResponseDto.listFromJson(json[r'assets'])!, | ||||||
|         album: AlbumResponseDto.fromJson(json[r'album']), |         album: AlbumResponseDto.fromJson(json[r'album']), | ||||||
|         allowUpload: mapValueOfType<bool>(json, r'allowUpload')!, |         allowUpload: mapValueOfType<bool>(json, r'allowUpload')!, | ||||||
|  |         allowDownload: mapValueOfType<bool>(json, r'allowDownload')!, | ||||||
|  |         showExif: mapValueOfType<bool>(json, r'showExif')!, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
| @ -200,6 +214,8 @@ class SharedLinkResponseDto { | |||||||
|     'expiresAt', |     'expiresAt', | ||||||
|     'assets', |     'assets', | ||||||
|     'allowUpload', |     'allowUpload', | ||||||
|  |     'allowDownload', | ||||||
|  |     'showExif', | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -31,6 +31,16 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // bool allowDownload | ||||||
|  |     test('to test the property `allowDownload`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // bool showExif | ||||||
|  |     test('to test the property `showExif`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     // String description |     // String description | ||||||
|     test('to test the property `description`', () async { |     test('to test the property `description`', () async { | ||||||
|       // TODO |       // TODO | ||||||
|  | |||||||
| @ -31,6 +31,16 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // bool allowDownload | ||||||
|  |     test('to test the property `allowDownload`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // bool showExif | ||||||
|  |     test('to test the property `showExif`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     // String description |     // String description | ||||||
|     test('to test the property `description`', () async { |     test('to test the property `description`', () async { | ||||||
|       // TODO |       // TODO | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								mobile/openapi/test/edit_shared_link_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/test/edit_shared_link_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @ -31,6 +31,16 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // bool allowDownload | ||||||
|  |     test('to test the property `allowDownload`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // bool showExif | ||||||
|  |     test('to test the property `showExif`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     // bool isEditExpireTime |     // bool isEditExpireTime | ||||||
|     test('to test the property `isEditExpireTime`', () async { |     test('to test the property `isEditExpireTime`', () async { | ||||||
|       // TODO |       // TODO | ||||||
|  | |||||||
| @ -66,6 +66,16 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // bool allowDownload | ||||||
|  |     test('to test the property `allowDownload`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // bool showExif | ||||||
|  |     test('to test the property `showExif`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -140,6 +140,8 @@ export class AlbumController { | |||||||
|     @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, |     @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, | ||||||
|     @Response({ passthrough: true }) res: Res, |     @Response({ passthrough: true }) res: Res, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|  |     this.albumService.checkDownloadAccess(authUser); | ||||||
|  | 
 | ||||||
|     const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive( |     const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive( | ||||||
|       authUser, |       authUser, | ||||||
|       albumId, |       albumId, | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ import { DownloadService } from '../../modules/download/download.service'; | |||||||
| import { DownloadDto } from '../asset/dto/download-library.dto'; | import { DownloadDto } from '../asset/dto/download-library.dto'; | ||||||
| import { ShareCore } from '../share/share.core'; | import { ShareCore } from '../share/share.core'; | ||||||
| import { ISharedLinkRepository } from '../share/shared-link.repository'; | import { ISharedLinkRepository } from '../share/shared-link.repository'; | ||||||
| import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; | import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; | ||||||
| import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto'; | import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| 
 | 
 | ||||||
| @ -210,8 +210,14 @@ export class AlbumService { | |||||||
|       album: album, |       album: album, | ||||||
|       assets: [], |       assets: [], | ||||||
|       description: dto.description, |       description: dto.description, | ||||||
|  |       allowDownload: dto.allowDownload, | ||||||
|  |       showExif: dto.showExif, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return mapSharedLinkToResponseDto(sharedLink); |     return mapSharedLink(sharedLink); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   checkDownloadAccess(authUser: AuthUserDto) { | ||||||
|  |     this.shareCore.checkDownloadAccess(authUser); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -13,6 +13,14 @@ export class CreateAlbumShareLinkDto { | |||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   allowUpload?: boolean; |   allowUpload?: boolean; | ||||||
| 
 | 
 | ||||||
|  |   @IsBoolean() | ||||||
|  |   @IsOptional() | ||||||
|  |   allowDownload?: boolean; | ||||||
|  | 
 | ||||||
|  |   @IsBoolean() | ||||||
|  |   @IsOptional() | ||||||
|  |   showExif?: boolean; | ||||||
|  | 
 | ||||||
|   @IsString() |   @IsString() | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   description?: string; |   description?: string; | ||||||
|  | |||||||
| @ -97,6 +97,7 @@ export class AssetController { | |||||||
|     @Query(new ValidationPipe({ transform: true })) query: ServeFileDto, |     @Query(new ValidationPipe({ transform: true })) query: ServeFileDto, | ||||||
|     @Param('assetId') assetId: string, |     @Param('assetId') assetId: string, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|  |     this.assetService.checkDownloadAccess(authUser); | ||||||
|     await this.assetService.checkAssetsAccess(authUser, [assetId]); |     await this.assetService.checkAssetsAccess(authUser, [assetId]); | ||||||
|     return this.assetService.downloadFile(query, assetId, res); |     return this.assetService.downloadFile(query, assetId, res); | ||||||
|   } |   } | ||||||
| @ -108,6 +109,7 @@ export class AssetController { | |||||||
|     @Response({ passthrough: true }) res: Res, |     @Response({ passthrough: true }) res: Res, | ||||||
|     @Body(new ValidationPipe()) dto: DownloadFilesDto, |     @Body(new ValidationPipe()) dto: DownloadFilesDto, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|  |     this.assetService.checkDownloadAccess(authUser); | ||||||
|     await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]); |     await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]); | ||||||
|     const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto); |     const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto); | ||||||
|     res.attachment(fileName); |     res.attachment(fileName); | ||||||
| @ -117,6 +119,9 @@ export class AssetController { | |||||||
|     return stream; |     return stream; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Current this is not used in any UI element | ||||||
|  |    */ | ||||||
|   @Authenticated({ isShared: true }) |   @Authenticated({ isShared: true }) | ||||||
|   @Get('/download-library') |   @Get('/download-library') | ||||||
|   async downloadLibrary( |   async downloadLibrary( | ||||||
| @ -124,6 +129,7 @@ export class AssetController { | |||||||
|     @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, |     @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, | ||||||
|     @Response({ passthrough: true }) res: Res, |     @Response({ passthrough: true }) res: Res, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|  |     this.assetService.checkDownloadAccess(authUser); | ||||||
|     const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadLibrary(authUser, dto); |     const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadLibrary(authUser, dto); | ||||||
|     res.attachment(fileName); |     res.attachment(fileName); | ||||||
|     res.setHeader(IMMICH_CONTENT_LENGTH_HINT, fileSize); |     res.setHeader(IMMICH_CONTENT_LENGTH_HINT, fileSize); | ||||||
| @ -143,7 +149,7 @@ export class AssetController { | |||||||
|     @Param('assetId') assetId: string, |     @Param('assetId') assetId: string, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|     await this.assetService.checkAssetsAccess(authUser, [assetId]); |     await this.assetService.checkAssetsAccess(authUser, [assetId]); | ||||||
|     return this.assetService.serveFile(assetId, query, res, headers); |     return this.assetService.serveFile(authUser, assetId, query, res, headers); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @Authenticated({ isShared: true }) |   @Authenticated({ isShared: true }) | ||||||
| @ -246,7 +252,7 @@ export class AssetController { | |||||||
|     @Param('assetId') assetId: string, |     @Param('assetId') assetId: string, | ||||||
|   ): Promise<AssetResponseDto> { |   ): Promise<AssetResponseDto> { | ||||||
|     await this.assetService.checkAssetsAccess(authUser, [assetId]); |     await this.assetService.checkAssetsAccess(authUser, [assetId]); | ||||||
|     return await this.assetService.getAssetById(assetId); |     return await this.assetService.getAssetById(authUser, assetId); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
| @ -274,14 +280,14 @@ export class AssetController { | |||||||
|     const deleteAssetList: AssetResponseDto[] = []; |     const deleteAssetList: AssetResponseDto[] = []; | ||||||
| 
 | 
 | ||||||
|     for (const id of assetIds.ids) { |     for (const id of assetIds.ids) { | ||||||
|       const assets = await this.assetService.getAssetById(id); |       const assets = await this.assetService.getAssetById(authUser, id); | ||||||
|       if (!assets) { |       if (!assets) { | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|       deleteAssetList.push(assets); |       deleteAssetList.push(assets); | ||||||
| 
 | 
 | ||||||
|       if (assets.livePhotoVideoId) { |       if (assets.livePhotoVideoId) { | ||||||
|         const livePhotoVideo = await this.assetService.getAssetById(assets.livePhotoVideoId); |         const livePhotoVideo = await this.assetService.getAssetById(authUser, assets.livePhotoVideoId); | ||||||
|         if (livePhotoVideo) { |         if (livePhotoVideo) { | ||||||
|           deleteAssetList.push(livePhotoVideo); |           deleteAssetList.push(livePhotoVideo); | ||||||
|           assetIds.ids = [...assetIds.ids, livePhotoVideo.id]; |           assetIds.ids = [...assetIds.ids, livePhotoVideo.id]; | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ import { SearchAssetDto } from './dto/search-asset.dto'; | |||||||
| import fs from 'fs/promises'; | import fs from 'fs/promises'; | ||||||
| import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; | import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; | ||||||
| import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; | import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; | ||||||
| import { AssetResponseDto, mapAsset } from './response-dto/asset-response.dto'; | import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from './response-dto/asset-response.dto'; | ||||||
| import { CreateAssetDto } from './dto/create-asset.dto'; | import { CreateAssetDto } from './dto/create-asset.dto'; | ||||||
| import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; | import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; | ||||||
| import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; | import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; | ||||||
| @ -52,7 +52,7 @@ import { ShareCore } from '../share/share.core'; | |||||||
| import { ISharedLinkRepository } from '../share/shared-link.repository'; | import { ISharedLinkRepository } from '../share/shared-link.repository'; | ||||||
| import { DownloadFilesDto } from './dto/download-files.dto'; | import { DownloadFilesDto } from './dto/download-files.dto'; | ||||||
| import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; | import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; | ||||||
| import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; | import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; | ||||||
| import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto'; | import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto'; | ||||||
| 
 | 
 | ||||||
| const fileInfo = promisify(stat); | const fileInfo = promisify(stat); | ||||||
| @ -215,10 +215,15 @@ export class AssetService { | |||||||
|     return assets.map((asset) => mapAsset(asset)); |     return assets.map((asset) => mapAsset(asset)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async getAssetById(assetId: string): Promise<AssetResponseDto> { |   public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> { | ||||||
|  |     const allowExif = this.getExifPermission(authUser); | ||||||
|     const asset = await this._assetRepository.getById(assetId); |     const asset = await this._assetRepository.getById(assetId); | ||||||
| 
 | 
 | ||||||
|  |     if (allowExif) { | ||||||
|       return mapAsset(asset); |       return mapAsset(asset); | ||||||
|  |     } else { | ||||||
|  |       return mapAssetWithoutExif(asset); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async updateAsset(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> { |   public async updateAsset(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> { | ||||||
| @ -356,7 +361,15 @@ export class AssetService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async serveFile(assetId: string, query: ServeFileDto, res: Res, headers: Record<string, string>) { |   public async serveFile( | ||||||
|  |     authUser: AuthUserDto, | ||||||
|  |     assetId: string, | ||||||
|  |     query: ServeFileDto, | ||||||
|  |     res: Res, | ||||||
|  |     headers: Record<string, string>, | ||||||
|  |   ) { | ||||||
|  |     const allowOriginalFile = !authUser.isPublicUser || authUser.isAllowDownload; | ||||||
|  | 
 | ||||||
|     let fileReadStream: ReadStream; |     let fileReadStream: ReadStream; | ||||||
|     const asset = await this._assetRepository.getById(assetId); |     const asset = await this._assetRepository.getById(assetId); | ||||||
| 
 | 
 | ||||||
| @ -390,7 +403,7 @@ export class AssetService { | |||||||
|         /** |         /** | ||||||
|          * Serve thumbnail image for both web and mobile app |          * Serve thumbnail image for both web and mobile app | ||||||
|          */ |          */ | ||||||
|         if (!query.isThumb) { |         if (!query.isThumb && allowOriginalFile) { | ||||||
|           res.set({ |           res.set({ | ||||||
|             'Content-Type': asset.mimeType, |             'Content-Type': asset.mimeType, | ||||||
|           }); |           }); | ||||||
| @ -676,6 +689,10 @@ export class AssetService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   checkDownloadAccess(authUser: AuthUserDto) { | ||||||
|  |     this.shareCore.checkDownloadAccess(authUser); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise<SharedLinkResponseDto> { |   async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise<SharedLinkResponseDto> { | ||||||
|     const assets = []; |     const assets = []; | ||||||
| 
 | 
 | ||||||
| @ -691,9 +708,11 @@ export class AssetService { | |||||||
|       allowUpload: dto.allowUpload, |       allowUpload: dto.allowUpload, | ||||||
|       assets: assets, |       assets: assets, | ||||||
|       description: dto.description, |       description: dto.description, | ||||||
|  |       allowDownload: dto.allowDownload, | ||||||
|  |       showExif: dto.showExif, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return mapSharedLinkToResponseDto(sharedLink); |     return mapSharedLink(sharedLink); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async updateAssetsInSharedLink( |   async updateAssetsInSharedLink( | ||||||
| @ -709,7 +728,11 @@ export class AssetService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const updatedLink = await this.shareCore.updateAssetsInSharedLink(authUser.sharedLinkId, assets); |     const updatedLink = await this.shareCore.updateAssetsInSharedLink(authUser.sharedLinkId, assets); | ||||||
|     return mapSharedLinkToResponseDto(updatedLink); |     return mapSharedLink(updatedLink); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getExifPermission(authUser: AuthUserDto) { | ||||||
|  |     return !authUser.isPublicUser || authUser.isShowExif; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -25,6 +25,14 @@ export class CreateAssetsShareLinkDto { | |||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   allowUpload?: boolean; |   allowUpload?: boolean; | ||||||
| 
 | 
 | ||||||
|  |   @IsBoolean() | ||||||
|  |   @IsOptional() | ||||||
|  |   allowDownload?: boolean; | ||||||
|  | 
 | ||||||
|  |   @IsBoolean() | ||||||
|  |   @IsOptional() | ||||||
|  |   showExif?: boolean; | ||||||
|  | 
 | ||||||
|   @IsString() |   @IsString() | ||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   description?: string; |   description?: string; | ||||||
|  | |||||||
| @ -49,3 +49,26 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { | |||||||
|     tags: entity.tags?.map(mapTag), |     tags: entity.tags?.map(mapTag), | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto { | ||||||
|  |   return { | ||||||
|  |     id: entity.id, | ||||||
|  |     deviceAssetId: entity.deviceAssetId, | ||||||
|  |     ownerId: entity.userId, | ||||||
|  |     deviceId: entity.deviceId, | ||||||
|  |     type: entity.type, | ||||||
|  |     originalPath: entity.originalPath, | ||||||
|  |     resizePath: entity.resizePath, | ||||||
|  |     createdAt: entity.createdAt, | ||||||
|  |     modifiedAt: entity.modifiedAt, | ||||||
|  |     isFavorite: entity.isFavorite, | ||||||
|  |     mimeType: entity.mimeType, | ||||||
|  |     webpPath: entity.webpPath, | ||||||
|  |     encodedVideoPath: entity.encodedVideoPath, | ||||||
|  |     duration: entity.duration ?? '0:00:00.00000', | ||||||
|  |     exifInfo: undefined, | ||||||
|  |     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, | ||||||
|  |     livePhotoVideoId: entity.livePhotoVideoId, | ||||||
|  |     tags: entity.tags?.map(mapTag), | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | |||||||
| @ -8,4 +8,6 @@ export class CreateSharedLinkDto { | |||||||
|   assets!: AssetEntity[]; |   assets!: AssetEntity[]; | ||||||
|   album?: AlbumEntity; |   album?: AlbumEntity; | ||||||
|   allowUpload?: boolean; |   allowUpload?: boolean; | ||||||
|  |   allowDownload?: boolean; | ||||||
|  |   showExif?: boolean; | ||||||
| } | } | ||||||
|  | |||||||
| @ -10,6 +10,12 @@ export class EditSharedLinkDto { | |||||||
|   @IsOptional() |   @IsOptional() | ||||||
|   allowUpload?: boolean; |   allowUpload?: boolean; | ||||||
| 
 | 
 | ||||||
|  |   @IsOptional() | ||||||
|  |   allowDownload?: boolean; | ||||||
|  | 
 | ||||||
|  |   @IsOptional() | ||||||
|  |   showExif?: boolean; | ||||||
|  | 
 | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   isEditExpireTime?: boolean; |   isEditExpireTime?: boolean; | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { SharedLinkEntity, SharedLinkType } from '@app/infra'; | |||||||
| import { ApiProperty } from '@nestjs/swagger'; | import { ApiProperty } from '@nestjs/swagger'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album/response-dto/album-response.dto'; | import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album/response-dto/album-response.dto'; | ||||||
| import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto'; | import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../../asset/response-dto/asset-response.dto'; | ||||||
| 
 | 
 | ||||||
| export class SharedLinkResponseDto { | export class SharedLinkResponseDto { | ||||||
|   id!: string; |   id!: string; | ||||||
| @ -17,9 +17,11 @@ export class SharedLinkResponseDto { | |||||||
|   assets!: AssetResponseDto[]; |   assets!: AssetResponseDto[]; | ||||||
|   album?: AlbumResponseDto; |   album?: AlbumResponseDto; | ||||||
|   allowUpload!: boolean; |   allowUpload!: boolean; | ||||||
|  |   allowDownload!: boolean; | ||||||
|  |   showExif!: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapSharedLinkToResponseDto(sharedLink: SharedLinkEntity): SharedLinkResponseDto { | export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto { | ||||||
|   const linkAssets = sharedLink.assets || []; |   const linkAssets = sharedLink.assets || []; | ||||||
|   const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo); |   const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo); | ||||||
| 
 | 
 | ||||||
| @ -36,5 +38,29 @@ export function mapSharedLinkToResponseDto(sharedLink: SharedLinkEntity): Shared | |||||||
|     assets: assets.map(mapAsset), |     assets: assets.map(mapAsset), | ||||||
|     album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined, |     album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined, | ||||||
|     allowUpload: sharedLink.allowUpload, |     allowUpload: sharedLink.allowUpload, | ||||||
|  |     allowDownload: sharedLink.allowDownload, | ||||||
|  |     showExif: sharedLink.showExif, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto { | ||||||
|  |   const linkAssets = sharedLink.assets || []; | ||||||
|  |   const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo); | ||||||
|  | 
 | ||||||
|  |   const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     id: sharedLink.id, | ||||||
|  |     description: sharedLink.description, | ||||||
|  |     userId: sharedLink.userId, | ||||||
|  |     key: sharedLink.key.toString('hex'), | ||||||
|  |     type: sharedLink.type, | ||||||
|  |     createdAt: sharedLink.createdAt, | ||||||
|  |     expiresAt: sharedLink.expiresAt, | ||||||
|  |     assets: assets.map(mapAssetWithoutExif), | ||||||
|  |     album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined, | ||||||
|  |     allowUpload: sharedLink.allowUpload, | ||||||
|  |     allowDownload: sharedLink.allowDownload, | ||||||
|  |     showExif: sharedLink.showExif, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ export class ShareController { | |||||||
|   @Authenticated() |   @Authenticated() | ||||||
|   @Get(':id') |   @Get(':id') | ||||||
|   getSharedLinkById(@Param('id') id: string): Promise<SharedLinkResponseDto> { |   getSharedLinkById(@Param('id') id: string): Promise<SharedLinkResponseDto> { | ||||||
|     return this.shareService.getById(id); |     return this.shareService.getById(id, true); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @Authenticated() |   @Authenticated() | ||||||
|  | |||||||
| @ -2,9 +2,10 @@ import { SharedLinkEntity } from '@app/infra'; | |||||||
| import { CreateSharedLinkDto } from './dto/create-shared-link.dto'; | import { CreateSharedLinkDto } from './dto/create-shared-link.dto'; | ||||||
| import { ISharedLinkRepository } from './shared-link.repository'; | import { ISharedLinkRepository } from './shared-link.repository'; | ||||||
| import crypto from 'node:crypto'; | import crypto from 'node:crypto'; | ||||||
| import { BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common'; | import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common'; | ||||||
| import { AssetEntity } from '@app/infra'; | import { AssetEntity } from '@app/infra'; | ||||||
| import { EditSharedLinkDto } from './dto/edit-shared-link.dto'; | import { EditSharedLinkDto } from './dto/edit-shared-link.dto'; | ||||||
|  | import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||||
| 
 | 
 | ||||||
| export class ShareCore { | export class ShareCore { | ||||||
|   readonly logger = new Logger(ShareCore.name); |   readonly logger = new Logger(ShareCore.name); | ||||||
| @ -24,6 +25,8 @@ export class ShareCore { | |||||||
|       sharedLink.assets = dto.assets; |       sharedLink.assets = dto.assets; | ||||||
|       sharedLink.album = dto.album; |       sharedLink.album = dto.album; | ||||||
|       sharedLink.allowUpload = dto.allowUpload ?? false; |       sharedLink.allowUpload = dto.allowUpload ?? false; | ||||||
|  |       sharedLink.allowDownload = dto.allowDownload ?? true; | ||||||
|  |       sharedLink.showExif = dto.showExif ?? true; | ||||||
| 
 | 
 | ||||||
|       return this.sharedLinkRepository.create(sharedLink); |       return this.sharedLinkRepository.create(sharedLink); | ||||||
|     } catch (error: any) { |     } catch (error: any) { | ||||||
| @ -74,6 +77,8 @@ export class ShareCore { | |||||||
| 
 | 
 | ||||||
|     link.description = dto.description ?? link.description; |     link.description = dto.description ?? link.description; | ||||||
|     link.allowUpload = dto.allowUpload ?? link.allowUpload; |     link.allowUpload = dto.allowUpload ?? link.allowUpload; | ||||||
|  |     link.allowDownload = dto.allowDownload ?? link.allowDownload; | ||||||
|  |     link.showExif = dto.showExif ?? link.showExif; | ||||||
| 
 | 
 | ||||||
|     if (dto.isEditExpireTime && dto.expiredAt) { |     if (dto.isEditExpireTime && dto.expiredAt) { | ||||||
|       link.expiresAt = dto.expiredAt; |       link.expiresAt = dto.expiredAt; | ||||||
| @ -87,4 +92,10 @@ export class ShareCore { | |||||||
|   async hasAssetAccess(id: string, assetId: string): Promise<boolean> { |   async hasAssetAccess(id: string, assetId: string): Promise<boolean> { | ||||||
|     return this.sharedLinkRepository.hasAssetAccess(id, assetId); |     return this.sharedLinkRepository.hasAssetAccess(id, assetId); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   checkDownloadAccess(user: AuthUserDto) { | ||||||
|  |     if (user.isPublicUser && !user.isAllowDownload) { | ||||||
|  |       throw new ForbiddenException(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ import { | |||||||
| import { UserService } from '@app/domain'; | import { UserService } from '@app/domain'; | ||||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||||
| import { EditSharedLinkDto } from './dto/edit-shared-link.dto'; | import { EditSharedLinkDto } from './dto/edit-shared-link.dto'; | ||||||
| import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from './response-dto/shared-link-response.dto'; | import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto/shared-link-response.dto'; | ||||||
| import { ShareCore } from './share.core'; | import { ShareCore } from './share.core'; | ||||||
| import { ISharedLinkRepository } from './shared-link.repository'; | import { ISharedLinkRepository } from './shared-link.repository'; | ||||||
| 
 | 
 | ||||||
| @ -39,6 +39,8 @@ export class ShareService { | |||||||
|             isPublicUser: true, |             isPublicUser: true, | ||||||
|             sharedLinkId: link.id, |             sharedLinkId: link.id, | ||||||
|             isAllowUpload: link.allowUpload, |             isAllowUpload: link.allowUpload, | ||||||
|  |             isAllowDownload: link.allowDownload, | ||||||
|  |             isShowExif: link.showExif, | ||||||
|           }; |           }; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @ -48,7 +50,7 @@ export class ShareService { | |||||||
| 
 | 
 | ||||||
|   async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> { |   async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> { | ||||||
|     const links = await this.shareCore.getSharedLinks(authUser.id); |     const links = await this.shareCore.getSharedLinks(authUser.id); | ||||||
|     return links.map(mapSharedLinkToResponseDto); |     return links.map(mapSharedLink); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> { |   async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> { | ||||||
| @ -56,15 +58,25 @@ export class ShareService { | |||||||
|       throw new ForbiddenException(); |       throw new ForbiddenException(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return this.getById(authUser.sharedLinkId); |     let allowExif = true; | ||||||
|  |     if (authUser.isShowExif != undefined) { | ||||||
|  |       allowExif = authUser.isShowExif; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   async getById(id: string): Promise<SharedLinkResponseDto> { |     return this.getById(authUser.sharedLinkId, allowExif); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getById(id: string, allowExif: boolean): Promise<SharedLinkResponseDto> { | ||||||
|     const link = await this.shareCore.getSharedLinkById(id); |     const link = await this.shareCore.getSharedLinkById(id); | ||||||
|     if (!link) { |     if (!link) { | ||||||
|       throw new BadRequestException('Shared link not found'); |       throw new BadRequestException('Shared link not found'); | ||||||
|     } |     } | ||||||
|     return mapSharedLinkToResponseDto(link); | 
 | ||||||
|  |     if (allowExif) { | ||||||
|  |       return mapSharedLink(link); | ||||||
|  |     } else { | ||||||
|  |       return mapSharedLinkWithNoExif(link); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async remove(id: string, userId: string): Promise<string> { |   async remove(id: string, userId: string): Promise<string> { | ||||||
| @ -77,11 +89,11 @@ export class ShareService { | |||||||
|     if (!link) { |     if (!link) { | ||||||
|       throw new BadRequestException('Shared link not found'); |       throw new BadRequestException('Shared link not found'); | ||||||
|     } |     } | ||||||
|     return mapSharedLinkToResponseDto(link); |     return mapSharedLink(link); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async edit(id: string, authUser: AuthUserDto, dto: EditSharedLinkDto) { |   async edit(id: string, authUser: AuthUserDto, dto: EditSharedLinkDto) { | ||||||
|     const link = await this.shareCore.updateSharedLink(id, authUser.id, dto); |     const link = await this.shareCore.updateSharedLink(id, authUser.id, dto); | ||||||
|     return mapSharedLinkToResponseDto(link); |     return mapSharedLink(link); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -139,7 +139,6 @@ export class MetadataExtractionProcessor { | |||||||
|   async extractExifInfo(job: Job<IExifExtractionProcessor>) { |   async extractExifInfo(job: Job<IExifExtractionProcessor>) { | ||||||
|     try { |     try { | ||||||
|       const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data; |       const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data; | ||||||
| 
 |  | ||||||
|       const exifData = await exiftool.read(asset.originalPath).catch((e) => { |       const exifData = await exiftool.read(asset.originalPath).catch((e) => { | ||||||
|         this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`); |         this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`); | ||||||
|         return null; |         return null; | ||||||
|  | |||||||
| @ -736,7 +736,7 @@ | |||||||
|     "/asset/download-library": { |     "/asset/download-library": { | ||||||
|       "get": { |       "get": { | ||||||
|         "operationId": "downloadLibrary", |         "operationId": "downloadLibrary", | ||||||
|         "description": "", |         "description": "Current this is not used in any UI element", | ||||||
|         "parameters": [ |         "parameters": [ | ||||||
|           { |           { | ||||||
|             "name": "skip", |             "name": "skip", | ||||||
| @ -3786,6 +3786,12 @@ | |||||||
|           "allowUpload": { |           "allowUpload": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|           }, |           }, | ||||||
|  |           "allowDownload": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "showExif": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|           "description": { |           "description": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           } |           } | ||||||
| @ -3887,6 +3893,12 @@ | |||||||
|           }, |           }, | ||||||
|           "allowUpload": { |           "allowUpload": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "allowDownload": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "showExif": { | ||||||
|  |             "type": "boolean" | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "required": [ |         "required": [ | ||||||
| @ -3897,7 +3909,9 @@ | |||||||
|           "createdAt", |           "createdAt", | ||||||
|           "expiresAt", |           "expiresAt", | ||||||
|           "assets", |           "assets", | ||||||
|           "allowUpload" |           "allowUpload", | ||||||
|  |           "allowDownload", | ||||||
|  |           "showExif" | ||||||
|         ] |         ] | ||||||
|       }, |       }, | ||||||
|       "UpdateAssetsToSharedLinkDto": { |       "UpdateAssetsToSharedLinkDto": { | ||||||
| @ -3926,6 +3940,12 @@ | |||||||
|           "allowUpload": { |           "allowUpload": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|           }, |           }, | ||||||
|  |           "allowDownload": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "showExif": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|           "isEditExpireTime": { |           "isEditExpireTime": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|           } |           } | ||||||
| @ -4085,6 +4105,12 @@ | |||||||
|           "allowUpload": { |           "allowUpload": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|           }, |           }, | ||||||
|  |           "allowDownload": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "showExif": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|           "description": { |           "description": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           } |           } | ||||||
|  | |||||||
| @ -5,4 +5,6 @@ export class AuthUserDto { | |||||||
|   isPublicUser?: boolean; |   isPublicUser?: boolean; | ||||||
|   sharedLinkId?: string; |   sharedLinkId?: string; | ||||||
|   isAllowUpload?: boolean; |   isAllowUpload?: boolean; | ||||||
|  |   isAllowDownload?: boolean; | ||||||
|  |   isShowExif?: boolean; | ||||||
| } | } | ||||||
|  | |||||||
| @ -30,6 +30,12 @@ export class SharedLinkEntity { | |||||||
|   @Column({ type: 'boolean', default: false }) |   @Column({ type: 'boolean', default: false }) | ||||||
|   allowUpload!: boolean; |   allowUpload!: boolean; | ||||||
| 
 | 
 | ||||||
|  |   @Column({ type: 'boolean', default: true }) | ||||||
|  |   allowDownload!: boolean; | ||||||
|  | 
 | ||||||
|  |   @Column({ type: 'boolean', default: true }) | ||||||
|  |   showExif!: boolean; | ||||||
|  | 
 | ||||||
|   @ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks) |   @ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks) | ||||||
|   assets!: AssetEntity[]; |   assets!: AssetEntity[]; | ||||||
| 
 | 
 | ||||||
| @ -47,4 +53,4 @@ export enum SharedLinkType { | |||||||
|   INDIVIDUAL = 'INDIVIDUAL', |   INDIVIDUAL = 'INDIVIDUAL', | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // npm run typeorm -- migration:generate ./libs/database/src/AddSharedLinkTable -d libs/database/src/config/database.config.ts
 | // npm run typeorm -- migration:generate ./libs/infra/src/db/AddMorePermissionToSharedLink -d ./libs/infra/src/db/config/database.config.ts
 | ||||||
|  | |||||||
| @ -0,0 +1,15 @@ | |||||||
|  | import { MigrationInterface, QueryRunner } from 'typeorm'; | ||||||
|  | 
 | ||||||
|  | export class AddMorePermissionToSharedLink1673907194740 implements MigrationInterface { | ||||||
|  |   name = 'AddMorePermissionToSharedLink1673907194740'; | ||||||
|  | 
 | ||||||
|  |   public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |     await queryRunner.query(`ALTER TABLE "shared_links" ADD "allowDownload" boolean NOT NULL DEFAULT true`); | ||||||
|  |     await queryRunner.query(`ALTER TABLE "shared_links" ADD "showExif" boolean NOT NULL DEFAULT true`); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |     await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "showExif"`); | ||||||
|  |     await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "allowDownload"`); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										48
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @ -665,6 +665,18 @@ export interface CreateAlbumShareLinkDto { | |||||||
|      * @memberof CreateAlbumShareLinkDto |      * @memberof CreateAlbumShareLinkDto | ||||||
|      */ |      */ | ||||||
|     'allowUpload'?: boolean; |     'allowUpload'?: boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof CreateAlbumShareLinkDto | ||||||
|  |      */ | ||||||
|  |     'allowDownload'?: boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof CreateAlbumShareLinkDto | ||||||
|  |      */ | ||||||
|  |     'showExif'?: boolean; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {string} |      * @type {string} | ||||||
| @ -696,6 +708,18 @@ export interface CreateAssetsShareLinkDto { | |||||||
|      * @memberof CreateAssetsShareLinkDto |      * @memberof CreateAssetsShareLinkDto | ||||||
|      */ |      */ | ||||||
|     'allowUpload'?: boolean; |     'allowUpload'?: boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof CreateAssetsShareLinkDto | ||||||
|  |      */ | ||||||
|  |     'allowDownload'?: boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof CreateAssetsShareLinkDto | ||||||
|  |      */ | ||||||
|  |     'showExif'?: boolean; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {string} |      * @type {string} | ||||||
| @ -987,6 +1011,18 @@ export interface EditSharedLinkDto { | |||||||
|      * @memberof EditSharedLinkDto |      * @memberof EditSharedLinkDto | ||||||
|      */ |      */ | ||||||
|     'allowUpload'?: boolean; |     'allowUpload'?: boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof EditSharedLinkDto | ||||||
|  |      */ | ||||||
|  |     'allowDownload'?: boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof EditSharedLinkDto | ||||||
|  |      */ | ||||||
|  |     'showExif'?: boolean; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {boolean} |      * @type {boolean} | ||||||
| @ -1612,6 +1648,18 @@ export interface SharedLinkResponseDto { | |||||||
|      * @memberof SharedLinkResponseDto |      * @memberof SharedLinkResponseDto | ||||||
|      */ |      */ | ||||||
|     'allowUpload': boolean; |     'allowUpload': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SharedLinkResponseDto | ||||||
|  |      */ | ||||||
|  |     'allowDownload': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SharedLinkResponseDto | ||||||
|  |      */ | ||||||
|  |     'showExif': boolean; | ||||||
| } | } | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
|  | |||||||
| @ -320,6 +320,7 @@ | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} catch (e) { | 		} catch (e) { | ||||||
|  | 			$downloadAssets = {}; | ||||||
| 			console.error('Error downloading file ', e); | 			console.error('Error downloading file ', e); | ||||||
| 			notificationController.show({ | 			notificationController.show({ | ||||||
| 				type: NotificationType.Error, | 				type: NotificationType.Error, | ||||||
| @ -460,11 +461,13 @@ | |||||||
| 						<CircleIconButton title="Remove album" on:click={removeAlbum} logo={DeleteOutline} /> | 						<CircleIconButton title="Remove album" on:click={removeAlbum} logo={DeleteOutline} /> | ||||||
| 					{/if} | 					{/if} | ||||||
| 
 | 
 | ||||||
|  | 					{#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)} | ||||||
| 						<CircleIconButton | 						<CircleIconButton | ||||||
| 							title="Download" | 							title="Download" | ||||||
| 							on:click={() => downloadAlbum()} | 							on:click={() => downloadAlbum()} | ||||||
| 							logo={FolderDownloadOutline} | 							logo={FolderDownloadOutline} | ||||||
| 						/> | 						/> | ||||||
|  | 					{/if} | ||||||
| 
 | 
 | ||||||
| 					{#if !isPublicShared} | 					{#if !isPublicShared} | ||||||
| 						<CircleIconButton | 						<CircleIconButton | ||||||
| @ -534,11 +537,7 @@ | |||||||
| 		{/if} | 		{/if} | ||||||
| 
 | 
 | ||||||
| 		{#if album.assetCount > 0} | 		{#if album.assetCount > 0} | ||||||
| 			<GalleryViewer | 			<GalleryViewer assets={album.assets} {sharedLink} bind:selectedAssets={multiSelectAsset} /> | ||||||
| 				assets={album.assets} |  | ||||||
| 				key={sharedLink?.key ?? ''} |  | ||||||
| 				bind:selectedAssets={multiSelectAsset} |  | ||||||
| 			/> |  | ||||||
| 		{:else} | 		{:else} | ||||||
| 			<!-- Album is empty - Show asset selectection buttons --> | 			<!-- Album is empty - Show asset selectection buttons --> | ||||||
| 			<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center"> | 			<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center"> | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ | |||||||
| 	export let showCopyButton: boolean; | 	export let showCopyButton: boolean; | ||||||
| 	export let showMotionPlayButton: boolean; | 	export let showMotionPlayButton: boolean; | ||||||
| 	export let isMotionPhotoPlaying = false; | 	export let isMotionPhotoPlaying = false; | ||||||
|  | 	export let showDownloadButton: boolean; | ||||||
| 
 | 
 | ||||||
| 	const isOwner = asset.ownerId === $page.data.user?.id; | 	const isOwner = asset.ownerId === $page.data.user?.id; | ||||||
| 
 | 
 | ||||||
| @ -77,11 +78,14 @@ | |||||||
| 				}} | 				}} | ||||||
| 			/> | 			/> | ||||||
| 		{/if} | 		{/if} | ||||||
|  | 
 | ||||||
|  | 		{#if showDownloadButton} | ||||||
| 			<CircleIconButton | 			<CircleIconButton | ||||||
| 				logo={CloudDownloadOutline} | 				logo={CloudDownloadOutline} | ||||||
| 				on:click={() => dispatch('download')} | 				on:click={() => dispatch('download')} | ||||||
| 				title="Download" | 				title="Download" | ||||||
| 			/> | 			/> | ||||||
|  | 		{/if} | ||||||
| 		<CircleIconButton | 		<CircleIconButton | ||||||
| 			logo={InformationOutline} | 			logo={InformationOutline} | ||||||
| 			on:click={() => dispatch('showDetail')} | 			on:click={() => dispatch('showDetail')} | ||||||
|  | |||||||
| @ -10,7 +10,13 @@ | |||||||
| 	import { downloadAssets } from '$lib/stores/download'; | 	import { downloadAssets } from '$lib/stores/download'; | ||||||
| 	import VideoViewer from './video-viewer.svelte'; | 	import VideoViewer from './video-viewer.svelte'; | ||||||
| 	import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte'; | 	import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte'; | ||||||
| 	import { api, AssetResponseDto, AssetTypeEnum, AlbumResponseDto } from '@api'; | 	import { | ||||||
|  | 		api, | ||||||
|  | 		AssetResponseDto, | ||||||
|  | 		AssetTypeEnum, | ||||||
|  | 		AlbumResponseDto, | ||||||
|  | 		SharedLinkResponseDto | ||||||
|  | 	} from '@api'; | ||||||
| 	import { | 	import { | ||||||
| 		notificationController, | 		notificationController, | ||||||
| 		NotificationType | 		NotificationType | ||||||
| @ -22,6 +28,7 @@ | |||||||
| 	export let asset: AssetResponseDto; | 	export let asset: AssetResponseDto; | ||||||
| 	export let publicSharedKey = ''; | 	export let publicSharedKey = ''; | ||||||
| 	export let showNavigation = true; | 	export let showNavigation = true; | ||||||
|  | 	export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||||
| 
 | 
 | ||||||
| 	const dispatch = createEventDispatcher(); | 	const dispatch = createEventDispatcher(); | ||||||
| 	let halfLeftHover = false; | 	let halfLeftHover = false; | ||||||
| @ -31,6 +38,7 @@ | |||||||
| 	let isShowAlbumPicker = false; | 	let isShowAlbumPicker = false; | ||||||
| 	let addToSharedAlbum = true; | 	let addToSharedAlbum = true; | ||||||
| 	let shouldPlayMotionPhoto = false; | 	let shouldPlayMotionPhoto = false; | ||||||
|  | 	let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : true; | ||||||
| 	const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo.key); | 	const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo.key); | ||||||
| 
 | 
 | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| @ -166,6 +174,7 @@ | |||||||
| 				}, 2000); | 				}, 2000); | ||||||
| 			} | 			} | ||||||
| 		} catch (e) { | 		} catch (e) { | ||||||
|  | 			$downloadAssets = {}; | ||||||
| 			console.error('Error downloading file ', e); | 			console.error('Error downloading file ', e); | ||||||
| 			notificationController.show({ | 			notificationController.show({ | ||||||
| 				type: NotificationType.Error, | 				type: NotificationType.Error, | ||||||
| @ -247,6 +256,7 @@ | |||||||
| 			isMotionPhotoPlaying={shouldPlayMotionPhoto} | 			isMotionPhotoPlaying={shouldPlayMotionPhoto} | ||||||
| 			showCopyButton={asset.type === AssetTypeEnum.Image} | 			showCopyButton={asset.type === AssetTypeEnum.Image} | ||||||
| 			showMotionPlayButton={!!asset.livePhotoVideoId} | 			showMotionPlayButton={!!asset.livePhotoVideoId} | ||||||
|  | 			showDownloadButton={shouldShowDownloadButton} | ||||||
| 			on:goBack={closeViewer} | 			on:goBack={closeViewer} | ||||||
| 			on:showDetail={showDetailInfoHandler} | 			on:showDetail={showDetailInfoHandler} | ||||||
| 			on:download={handleDownload} | 			on:download={handleDownload} | ||||||
|  | |||||||
| @ -136,15 +136,17 @@ | |||||||
| 					/> | 					/> | ||||||
| 				{/if} | 				{/if} | ||||||
| 
 | 
 | ||||||
|  | 				{#if sharedLink?.allowDownload} | ||||||
| 					<CircleIconButton | 					<CircleIconButton | ||||||
| 						title="Download" | 						title="Download" | ||||||
| 						on:click={() => downloadAssets(true)} | 						on:click={() => downloadAssets(true)} | ||||||
| 						logo={FolderDownloadOutline} | 						logo={FolderDownloadOutline} | ||||||
| 					/> | 					/> | ||||||
|  | 				{/if} | ||||||
| 			</svelte:fragment> | 			</svelte:fragment> | ||||||
| 		</ControlAppBar> | 		</ControlAppBar> | ||||||
| 	{/if} | 	{/if} | ||||||
| 	<section class="flex flex-col my-[160px] px-6 sm:px-12 md:px-24 lg:px-40"> | 	<section class="flex flex-col my-[160px] px-6 sm:px-12 md:px-24 lg:px-40"> | ||||||
| 		<GalleryViewer {assets} key={sharedLink.key} bind:selectedAssets /> | 		<GalleryViewer {assets} {sharedLink} bind:selectedAssets /> | ||||||
| 	</section> | 	</section> | ||||||
| </section> | </section> | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ | |||||||
| 	<div | 	<div | ||||||
| 		use:clickOutside | 		use:clickOutside | ||||||
| 		on:outclick={() => dispatch('close')} | 		on:outclick={() => dispatch('close')} | ||||||
| 		class="bg-immich-bg dark:bg-immich-dark-gray dark:text-immich-dark-fg w-[450px] min-h-[200px] max-h-[500px] rounded-lg shadow-md" | 		class="bg-immich-bg dark:bg-immich-dark-gray dark:text-immich-dark-fg w-[450px] min-h-[200px] max-h-[600px] rounded-lg shadow-md" | ||||||
| 	> | 	> | ||||||
| 		<div class="flex justify-between place-items-center px-5 py-3"> | 		<div class="flex justify-between place-items-center px-5 py-3"> | ||||||
| 			<div> | 			<div> | ||||||
|  | |||||||
| @ -29,6 +29,8 @@ | |||||||
| 	let sharedLink = ''; | 	let sharedLink = ''; | ||||||
| 	let description = ''; | 	let description = ''; | ||||||
| 	let shouldChangeExpirationTime = false; | 	let shouldChangeExpirationTime = false; | ||||||
|  | 	let isAllowDownload = true; | ||||||
|  | 	let shouldShowExif = true; | ||||||
| 	const dispatch = createEventDispatcher(); | 	const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
| 	const expiredDateOption: ImmichDropDownOption = { | 	const expiredDateOption: ImmichDropDownOption = { | ||||||
| @ -42,6 +44,8 @@ | |||||||
| 				description = editingLink.description; | 				description = editingLink.description; | ||||||
| 			} | 			} | ||||||
| 			isAllowUpload = editingLink.allowUpload; | 			isAllowUpload = editingLink.allowUpload; | ||||||
|  | 			isAllowDownload = editingLink.allowDownload; | ||||||
|  | 			shouldShowExif = editingLink.showExif; | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| @ -58,7 +62,9 @@ | |||||||
| 					albumId: album.id, | 					albumId: album.id, | ||||||
| 					expiredAt: expirationDate, | 					expiredAt: expirationDate, | ||||||
| 					allowUpload: isAllowUpload, | 					allowUpload: isAllowUpload, | ||||||
| 					description: description | 					description: description, | ||||||
|  | 					allowDownload: isAllowDownload, | ||||||
|  | 					showExif: shouldShowExif | ||||||
| 				}); | 				}); | ||||||
| 				buildSharedLink(data); | 				buildSharedLink(data); | ||||||
| 			} else { | 			} else { | ||||||
| @ -66,7 +72,9 @@ | |||||||
| 					assetIds: sharedAssets.map((a) => a.id), | 					assetIds: sharedAssets.map((a) => a.id), | ||||||
| 					expiredAt: expirationDate, | 					expiredAt: expirationDate, | ||||||
| 					allowUpload: isAllowUpload, | 					allowUpload: isAllowUpload, | ||||||
| 					description: description | 					description: description, | ||||||
|  | 					allowDownload: isAllowDownload, | ||||||
|  | 					showExif: shouldShowExif | ||||||
| 				}); | 				}); | ||||||
| 				buildSharedLink(data); | 				buildSharedLink(data); | ||||||
| 			} | 			} | ||||||
| @ -132,7 +140,9 @@ | |||||||
| 					description: description, | 					description: description, | ||||||
| 					expiredAt: expirationDate, | 					expiredAt: expirationDate, | ||||||
| 					allowUpload: isAllowUpload, | 					allowUpload: isAllowUpload, | ||||||
| 					isEditExpireTime: shouldChangeExpirationTime | 					isEditExpireTime: shouldChangeExpirationTime, | ||||||
|  | 					allowDownload: isAllowDownload, | ||||||
|  | 					showExif: shouldShowExif | ||||||
| 				}); | 				}); | ||||||
| 
 | 
 | ||||||
| 				notificationController.show({ | 				notificationController.show({ | ||||||
| @ -185,12 +195,12 @@ | |||||||
| 			{/if} | 			{/if} | ||||||
| 		{/if} | 		{/if} | ||||||
| 
 | 
 | ||||||
| 		<div class="mt-6 mb-2"> | 		<div class="mt-4 mb-2"> | ||||||
| 			<p class="text-xs">LINK OPTIONS</p> | 			<p class="text-xs">LINK OPTIONS</p> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg"> | 		<div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg"> | ||||||
| 			<div class="flex flex-col"> | 			<div class="flex flex-col"> | ||||||
| 				<div class="mb-4"> | 				<div class="mb-2"> | ||||||
| 					<SettingInputField | 					<SettingInputField | ||||||
| 						inputType={SettingInputFieldType.TEXT} | 						inputType={SettingInputFieldType.TEXT} | ||||||
| 						label="Description" | 						label="Description" | ||||||
| @ -198,9 +208,19 @@ | |||||||
| 					/> | 					/> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<SettingSwitch bind:checked={isAllowUpload} title={'Allow public user to upload'} /> | 				<div class="my-3"> | ||||||
|  | 					<SettingSwitch bind:checked={shouldShowExif} title={'Show metadata'} /> | ||||||
|  | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<div class="text-sm mt-4"> | 				<div class="my-3"> | ||||||
|  | 					<SettingSwitch bind:checked={isAllowDownload} title={'Allow public user to download'} /> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="my-3"> | ||||||
|  | 					<SettingSwitch bind:checked={isAllowUpload} title={'Allow public user to upload'} /> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="text-sm"> | ||||||
| 					{#if editingLink} | 					{#if editingLink} | ||||||
| 						<p class="my-2 immich-form-label"> | 						<p class="my-2 immich-form-label"> | ||||||
| 							<SettingSwitch | 							<SettingSwitch | ||||||
|  | |||||||
| @ -1,13 +1,13 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { page } from '$app/stores'; | 	import { page } from '$app/stores'; | ||||||
| 	import { handleError } from '$lib/utils/handle-error'; | 	import { handleError } from '$lib/utils/handle-error'; | ||||||
| 	import { AssetResponseDto, ThumbnailFormat } from '@api'; | 	import { AssetResponseDto, SharedLinkResponseDto, ThumbnailFormat } from '@api'; | ||||||
| 
 | 
 | ||||||
| 	import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; | 	import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; | ||||||
| 	import ImmichThumbnail from '../../shared-components/immich-thumbnail.svelte'; | 	import ImmichThumbnail from '../../shared-components/immich-thumbnail.svelte'; | ||||||
| 
 | 
 | ||||||
| 	export let assets: AssetResponseDto[]; | 	export let assets: AssetResponseDto[]; | ||||||
| 	export let key: string; | 	export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||||
| 	export let selectedAssets: Set<AssetResponseDto> = new Set(); | 	export let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||||
| 
 | 
 | ||||||
| 	let isShowAssetViewer = false; | 	let isShowAssetViewer = false; | ||||||
| @ -96,7 +96,7 @@ | |||||||
| 			<ImmichThumbnail | 			<ImmichThumbnail | ||||||
| 				{asset} | 				{asset} | ||||||
| 				{thumbnailSize} | 				{thumbnailSize} | ||||||
| 				publicSharedKey={key} | 				publicSharedKey={sharedLink?.key} | ||||||
| 				format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp} | 				format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp} | ||||||
| 				on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} | 				on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} | ||||||
| 				on:select={selectAssetHandler} | 				on:select={selectAssetHandler} | ||||||
| @ -110,7 +110,8 @@ | |||||||
| {#if isShowAssetViewer} | {#if isShowAssetViewer} | ||||||
| 	<AssetViewer | 	<AssetViewer | ||||||
| 		asset={selectedAsset} | 		asset={selectedAsset} | ||||||
| 		publicSharedKey={key} | 		publicSharedKey={sharedLink?.key} | ||||||
|  | 		{sharedLink} | ||||||
| 		on:navigate-previous={navigateAssetBackward} | 		on:navigate-previous={navigateAssetBackward} | ||||||
| 		on:navigate-next={navigateAssetForward} | 		on:navigate-next={navigateAssetForward} | ||||||
| 		on:close={closeViewer} | 		on:close={closeViewer} | ||||||
|  | |||||||
| @ -122,12 +122,28 @@ | |||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<div class="info-bottom"> | 		<div class="info-bottom flex gap-4"> | ||||||
| 			{#if link.allowUpload} | 			{#if link.allowUpload} | ||||||
|  | 				<div | ||||||
|  | 					class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[80px]" | ||||||
|  | 				> | ||||||
|  | 					Upload | ||||||
|  | 				</div> | ||||||
|  | 			{/if} | ||||||
|  | 
 | ||||||
|  | 			{#if link.allowDownload} | ||||||
| 				<div | 				<div | ||||||
| 					class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[100px]" | 					class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[100px]" | ||||||
| 				> | 				> | ||||||
| 					Allow upload | 					Download | ||||||
|  | 				</div> | ||||||
|  | 			{/if} | ||||||
|  | 
 | ||||||
|  | 			{#if link.showExif} | ||||||
|  | 				<div | ||||||
|  | 					class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[60px]" | ||||||
|  | 				> | ||||||
|  | 					EXIF | ||||||
| 				</div> | 				</div> | ||||||
| 			{/if} | 			{/if} | ||||||
| 		</div> | 		</div> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user