mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(server) user-defined storage structure (#1098)
[Breaking] newly uploaded file will conform to the default structure of `{uploadLocation}/{userId}/year/year-month-day/filename.ext`
			
			
This commit is contained in:
		
							
								
								
									
										6
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @@ -64,6 +64,8 @@ doc/SystemConfigApi.md | ||||
| doc/SystemConfigDto.md | ||||
| doc/SystemConfigFFmpegDto.md | ||||
| doc/SystemConfigOAuthDto.md | ||||
| doc/SystemConfigStorageTemplateDto.md | ||||
| doc/SystemConfigTemplateStorageOptionDto.md | ||||
| doc/TagApi.md | ||||
| doc/TagResponseDto.md | ||||
| doc/TagTypeEnum.md | ||||
| @@ -152,6 +154,8 @@ lib/model/smart_info_response_dto.dart | ||||
| lib/model/system_config_dto.dart | ||||
| lib/model/system_config_f_fmpeg_dto.dart | ||||
| lib/model/system_config_o_auth_dto.dart | ||||
| lib/model/system_config_storage_template_dto.dart | ||||
| lib/model/system_config_template_storage_option_dto.dart | ||||
| lib/model/tag_response_dto.dart | ||||
| lib/model/tag_type_enum.dart | ||||
| lib/model/thumbnail_format.dart | ||||
| @@ -227,6 +231,8 @@ test/system_config_api_test.dart | ||||
| test/system_config_dto_test.dart | ||||
| test/system_config_f_fmpeg_dto_test.dart | ||||
| test/system_config_o_auth_dto_test.dart | ||||
| test/system_config_storage_template_dto_test.dart | ||||
| test/system_config_template_storage_option_dto_test.dart | ||||
| test/tag_api_test.dart | ||||
| test/tag_response_dto_test.dart | ||||
| test/tag_type_enum_test.dart | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -3,7 +3,7 @@ Immich API | ||||
| 
 | ||||
| This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: | ||||
| 
 | ||||
| - API version: 1.38.0 | ||||
| - API version: 1.38.2 | ||||
| - Build package: org.openapitools.codegen.languages.DartClientCodegen | ||||
| 
 | ||||
| ## Requirements | ||||
| @@ -113,6 +113,7 @@ Class | Method | HTTP request | Description | ||||
| *ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |  | ||||
| *SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |  | ||||
| *SystemConfigApi* | [**getDefaults**](doc//SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults |  | ||||
| *SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |  | ||||
| *SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config |  | ||||
| *TagApi* | [**create**](doc//TagApi.md#create) | **POST** /tag |  | ||||
| *TagApi* | [**delete**](doc//TagApi.md#delete) | **DELETE** /tag/{id} |  | ||||
| @@ -186,6 +187,8 @@ Class | Method | HTTP request | Description | ||||
|  - [SystemConfigDto](doc//SystemConfigDto.md) | ||||
|  - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) | ||||
|  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) | ||||
|  - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) | ||||
|  - [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md) | ||||
|  - [TagResponseDto](doc//TagResponseDto.md) | ||||
|  - [TagTypeEnum](doc//TagTypeEnum.md) | ||||
|  - [ThumbnailFormat](doc//ThumbnailFormat.md) | ||||
|   | ||||
							
								
								
									
										44
									
								
								mobile/openapi/doc/SystemConfigApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								mobile/openapi/doc/SystemConfigApi.md
									
									
									
										generated
									
									
									
								
							| @@ -11,6 +11,7 @@ Method | HTTP request | Description | ||||
| ------------- | ------------- | ------------- | ||||
| [**getConfig**](SystemConfigApi.md#getconfig) | **GET** /system-config |  | ||||
| [**getDefaults**](SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults |  | ||||
| [**getStorageTemplateOptions**](SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |  | ||||
| [**updateConfig**](SystemConfigApi.md#updateconfig) | **PUT** /system-config |  | ||||
| 
 | ||||
| 
 | ||||
| @@ -100,6 +101,49 @@ This endpoint does not need any parameter. | ||||
| 
 | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **getStorageTemplateOptions** | ||||
| > SystemConfigTemplateStorageOptionDto getStorageTemplateOptions() | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ### Example | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| // TODO Configure HTTP Bearer authorization: bearer | ||||
| // Case 1. Use String Token | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); | ||||
| // Case 2. Use Function which generate token. | ||||
| // String yourTokenGeneratorFunction() { ... } | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = SystemConfigApi(); | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.getStorageTemplateOptions(); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling SystemConfigApi->getStorageTemplateOptions: $e\n'); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Parameters | ||||
| This endpoint does not need any parameter. | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| [**SystemConfigTemplateStorageOptionDto**](SystemConfigTemplateStorageOptionDto.md) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
| [bearer](../README.md#bearer) | ||||
| 
 | ||||
| ### HTTP request headers | ||||
| 
 | ||||
|  - **Content-Type**: Not defined | ||||
|  - **Accept**: application/json | ||||
| 
 | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **updateConfig** | ||||
| > SystemConfigDto updateConfig(systemConfigDto) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/SystemConfigDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/SystemConfigDto.md
									
									
									
										generated
									
									
									
								
							| @@ -10,6 +10,7 @@ Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) |  |  | ||||
| **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  |  | ||||
| **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.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) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										15
									
								
								mobile/openapi/doc/SystemConfigStorageTemplateDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								mobile/openapi/doc/SystemConfigStorageTemplateDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| # openapi.model.SystemConfigStorageTemplateDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **template** | **String** |  |  | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										21
									
								
								mobile/openapi/doc/SystemConfigTemplateStorageOptionDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								mobile/openapi/doc/SystemConfigTemplateStorageOptionDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| # openapi.model.SystemConfigTemplateStorageOptionDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **yearOptions** | **List<String>** |  | [default to const []] | ||||
| **monthOptions** | **List<String>** |  | [default to const []] | ||||
| **dayOptions** | **List<String>** |  | [default to const []] | ||||
| **hourOptions** | **List<String>** |  | [default to const []] | ||||
| **minuteOptions** | **List<String>** |  | [default to const []] | ||||
| **secondOptions** | **List<String>** |  | [default to const []] | ||||
| **presetOptions** | **List<String>** |  | [default to const []] | ||||
| 
 | ||||
| [[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/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -91,6 +91,8 @@ part 'model/smart_info_response_dto.dart'; | ||||
| part 'model/system_config_dto.dart'; | ||||
| part 'model/system_config_f_fmpeg_dto.dart'; | ||||
| part 'model/system_config_o_auth_dto.dart'; | ||||
| part 'model/system_config_storage_template_dto.dart'; | ||||
| part 'model/system_config_template_storage_option_dto.dart'; | ||||
| part 'model/tag_response_dto.dart'; | ||||
| part 'model/tag_type_enum.dart'; | ||||
| part 'model/thumbnail_format.dart'; | ||||
|   | ||||
							
								
								
									
										41
									
								
								mobile/openapi/lib/api/system_config_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										41
									
								
								mobile/openapi/lib/api/system_config_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -98,6 +98,47 @@ class SystemConfigApi { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Performs an HTTP 'GET /system-config/storage-template-options' operation and returns the [Response]. | ||||
|   Future<Response> getStorageTemplateOptionsWithHttpInfo() async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/system-config/storage-template-options'; | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>[]; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'GET', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<SystemConfigTemplateStorageOptionDto?> getStorageTemplateOptions() async { | ||||
|     final response = await getStorageTemplateOptionsWithHttpInfo(); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|     // When a remote server returns no body with a status of 204, we shall not decode it. | ||||
|     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" | ||||
|     // FormatException when trying to decode an empty string. | ||||
|     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { | ||||
|       return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigTemplateStorageOptionDto',) as SystemConfigTemplateStorageOptionDto; | ||||
|      | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Performs an HTTP 'PUT /system-config' operation and returns the [Response]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   | ||||
							
								
								
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -298,6 +298,10 @@ class ApiClient { | ||||
|           return SystemConfigFFmpegDto.fromJson(value); | ||||
|         case 'SystemConfigOAuthDto': | ||||
|           return SystemConfigOAuthDto.fromJson(value); | ||||
|         case 'SystemConfigStorageTemplateDto': | ||||
|           return SystemConfigStorageTemplateDto.fromJson(value); | ||||
|         case 'SystemConfigTemplateStorageOptionDto': | ||||
|           return SystemConfigTemplateStorageOptionDto.fromJson(value); | ||||
|         case 'TagResponseDto': | ||||
|           return TagResponseDto.fromJson(value); | ||||
|         case 'TagTypeEnum': | ||||
|   | ||||
							
								
								
									
										14
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -15,30 +15,36 @@ class SystemConfigDto { | ||||
|   SystemConfigDto({ | ||||
|     required this.ffmpeg, | ||||
|     required this.oauth, | ||||
|     required this.storageTemplate, | ||||
|   }); | ||||
| 
 | ||||
|   SystemConfigFFmpegDto ffmpeg; | ||||
| 
 | ||||
|   SystemConfigOAuthDto oauth; | ||||
| 
 | ||||
|   SystemConfigStorageTemplateDto storageTemplate; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && | ||||
|      other.ffmpeg == ffmpeg && | ||||
|      other.oauth == oauth; | ||||
|      other.oauth == oauth && | ||||
|      other.storageTemplate == storageTemplate; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (ffmpeg.hashCode) + | ||||
|     (oauth.hashCode); | ||||
|     (oauth.hashCode) + | ||||
|     (storageTemplate.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, oauth=$oauth]'; | ||||
|   String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, oauth=$oauth, storageTemplate=$storageTemplate]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|       _json[r'ffmpeg'] = ffmpeg; | ||||
|       _json[r'oauth'] = oauth; | ||||
|       _json[r'storageTemplate'] = storageTemplate; | ||||
|     return _json; | ||||
|   } | ||||
| 
 | ||||
| @@ -63,6 +69,7 @@ class SystemConfigDto { | ||||
|       return SystemConfigDto( | ||||
|         ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, | ||||
|         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, | ||||
|         storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
| @@ -114,6 +121,7 @@ class SystemConfigDto { | ||||
|   static const requiredKeys = <String>{ | ||||
|     'ffmpeg', | ||||
|     'oauth', | ||||
|     'storageTemplate', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										111
									
								
								mobile/openapi/lib/model/system_config_storage_template_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								mobile/openapi/lib/model/system_config_storage_template_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class SystemConfigStorageTemplateDto { | ||||
|   /// Returns a new [SystemConfigStorageTemplateDto] instance. | ||||
|   SystemConfigStorageTemplateDto({ | ||||
|     required this.template, | ||||
|   }); | ||||
| 
 | ||||
|   String template; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SystemConfigStorageTemplateDto && | ||||
|      other.template == template; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (template.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SystemConfigStorageTemplateDto[template=$template]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|       _json[r'template'] = template; | ||||
|     return _json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [SystemConfigStorageTemplateDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static SystemConfigStorageTemplateDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       // Ensure that the map contains the required keys. | ||||
|       // Note 1: the values aren't checked for validity beyond being non-null. | ||||
|       // Note 2: this code is stripped in release mode! | ||||
|       assert(() { | ||||
|         requiredKeys.forEach((key) { | ||||
|           assert(json.containsKey(key), 'Required key "SystemConfigStorageTemplateDto[$key]" is missing from JSON.'); | ||||
|           assert(json[key] != null, 'Required key "SystemConfigStorageTemplateDto[$key]" has a null value in JSON.'); | ||||
|         }); | ||||
|         return true; | ||||
|       }()); | ||||
| 
 | ||||
|       return SystemConfigStorageTemplateDto( | ||||
|         template: mapValueOfType<String>(json, r'template')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<SystemConfigStorageTemplateDto>? listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <SystemConfigStorageTemplateDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = SystemConfigStorageTemplateDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, SystemConfigStorageTemplateDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, SystemConfigStorageTemplateDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SystemConfigStorageTemplateDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of SystemConfigStorageTemplateDto-objects as value to a dart map | ||||
|   static Map<String, List<SystemConfigStorageTemplateDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<SystemConfigStorageTemplateDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SystemConfigStorageTemplateDto.listFromJson(entry.value, growable: growable,); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'template', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										173
									
								
								mobile/openapi/lib/model/system_config_template_storage_option_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								mobile/openapi/lib/model/system_config_template_storage_option_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class SystemConfigTemplateStorageOptionDto { | ||||
|   /// Returns a new [SystemConfigTemplateStorageOptionDto] instance. | ||||
|   SystemConfigTemplateStorageOptionDto({ | ||||
|     this.yearOptions = const [], | ||||
|     this.monthOptions = const [], | ||||
|     this.dayOptions = const [], | ||||
|     this.hourOptions = const [], | ||||
|     this.minuteOptions = const [], | ||||
|     this.secondOptions = const [], | ||||
|     this.presetOptions = const [], | ||||
|   }); | ||||
| 
 | ||||
|   List<String> yearOptions; | ||||
| 
 | ||||
|   List<String> monthOptions; | ||||
| 
 | ||||
|   List<String> dayOptions; | ||||
| 
 | ||||
|   List<String> hourOptions; | ||||
| 
 | ||||
|   List<String> minuteOptions; | ||||
| 
 | ||||
|   List<String> secondOptions; | ||||
| 
 | ||||
|   List<String> presetOptions; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SystemConfigTemplateStorageOptionDto && | ||||
|      other.yearOptions == yearOptions && | ||||
|      other.monthOptions == monthOptions && | ||||
|      other.dayOptions == dayOptions && | ||||
|      other.hourOptions == hourOptions && | ||||
|      other.minuteOptions == minuteOptions && | ||||
|      other.secondOptions == secondOptions && | ||||
|      other.presetOptions == presetOptions; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (yearOptions.hashCode) + | ||||
|     (monthOptions.hashCode) + | ||||
|     (dayOptions.hashCode) + | ||||
|     (hourOptions.hashCode) + | ||||
|     (minuteOptions.hashCode) + | ||||
|     (secondOptions.hashCode) + | ||||
|     (presetOptions.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SystemConfigTemplateStorageOptionDto[yearOptions=$yearOptions, monthOptions=$monthOptions, dayOptions=$dayOptions, hourOptions=$hourOptions, minuteOptions=$minuteOptions, secondOptions=$secondOptions, presetOptions=$presetOptions]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|       _json[r'yearOptions'] = yearOptions; | ||||
|       _json[r'monthOptions'] = monthOptions; | ||||
|       _json[r'dayOptions'] = dayOptions; | ||||
|       _json[r'hourOptions'] = hourOptions; | ||||
|       _json[r'minuteOptions'] = minuteOptions; | ||||
|       _json[r'secondOptions'] = secondOptions; | ||||
|       _json[r'presetOptions'] = presetOptions; | ||||
|     return _json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [SystemConfigTemplateStorageOptionDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static SystemConfigTemplateStorageOptionDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       // Ensure that the map contains the required keys. | ||||
|       // Note 1: the values aren't checked for validity beyond being non-null. | ||||
|       // Note 2: this code is stripped in release mode! | ||||
|       assert(() { | ||||
|         requiredKeys.forEach((key) { | ||||
|           assert(json.containsKey(key), 'Required key "SystemConfigTemplateStorageOptionDto[$key]" is missing from JSON.'); | ||||
|           assert(json[key] != null, 'Required key "SystemConfigTemplateStorageOptionDto[$key]" has a null value in JSON.'); | ||||
|         }); | ||||
|         return true; | ||||
|       }()); | ||||
| 
 | ||||
|       return SystemConfigTemplateStorageOptionDto( | ||||
|         yearOptions: json[r'yearOptions'] is List | ||||
|             ? (json[r'yearOptions'] as List).cast<String>() | ||||
|             : const [], | ||||
|         monthOptions: json[r'monthOptions'] is List | ||||
|             ? (json[r'monthOptions'] as List).cast<String>() | ||||
|             : const [], | ||||
|         dayOptions: json[r'dayOptions'] is List | ||||
|             ? (json[r'dayOptions'] as List).cast<String>() | ||||
|             : const [], | ||||
|         hourOptions: json[r'hourOptions'] is List | ||||
|             ? (json[r'hourOptions'] as List).cast<String>() | ||||
|             : const [], | ||||
|         minuteOptions: json[r'minuteOptions'] is List | ||||
|             ? (json[r'minuteOptions'] as List).cast<String>() | ||||
|             : const [], | ||||
|         secondOptions: json[r'secondOptions'] is List | ||||
|             ? (json[r'secondOptions'] as List).cast<String>() | ||||
|             : const [], | ||||
|         presetOptions: json[r'presetOptions'] is List | ||||
|             ? (json[r'presetOptions'] as List).cast<String>() | ||||
|             : const [], | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<SystemConfigTemplateStorageOptionDto>? listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <SystemConfigTemplateStorageOptionDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = SystemConfigTemplateStorageOptionDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, SystemConfigTemplateStorageOptionDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, SystemConfigTemplateStorageOptionDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SystemConfigTemplateStorageOptionDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of SystemConfigTemplateStorageOptionDto-objects as value to a dart map | ||||
|   static Map<String, List<SystemConfigTemplateStorageOptionDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<SystemConfigTemplateStorageOptionDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SystemConfigTemplateStorageOptionDto.listFromJson(entry.value, growable: growable,); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'yearOptions', | ||||
|     'monthOptions', | ||||
|     'dayOptions', | ||||
|     'hourOptions', | ||||
|     'minuteOptions', | ||||
|     'secondOptions', | ||||
|     'presetOptions', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/system_config_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/system_config_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -27,6 +27,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<SystemConfigTemplateStorageOptionDto> getStorageTemplateOptions() async | ||||
|     test('test getStorageTemplateOptions', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<SystemConfigDto> updateConfig(SystemConfigDto systemConfigDto) async | ||||
|     test('test updateConfig', () async { | ||||
|       // TODO | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/system_config_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/system_config_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -26,6 +26,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // SystemConfigStorageTemplateDto storageTemplate | ||||
|     test('to test the property `storageTemplate`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										27
									
								
								mobile/openapi/test/system_config_storage_template_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								mobile/openapi/test/system_config_storage_template_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for SystemConfigStorageTemplateDto | ||||
| void main() { | ||||
|   // final instance = SystemConfigStorageTemplateDto(); | ||||
| 
 | ||||
|   group('test SystemConfigStorageTemplateDto', () { | ||||
|     // String template | ||||
|     test('to test the property `template`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										57
									
								
								mobile/openapi/test/system_config_template_storage_option_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								mobile/openapi/test/system_config_template_storage_option_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for SystemConfigTemplateStorageOptionDto | ||||
| void main() { | ||||
|   // final instance = SystemConfigTemplateStorageOptionDto(); | ||||
| 
 | ||||
|   group('test SystemConfigTemplateStorageOptionDto', () { | ||||
|     // List<String> yearOptions (default value: const []) | ||||
|     test('to test the property `yearOptions`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<String> monthOptions (default value: const []) | ||||
|     test('to test the property `monthOptions`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<String> dayOptions (default value: const []) | ||||
|     test('to test the property `dayOptions`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<String> hourOptions (default value: const []) | ||||
|     test('to test the property `hourOptions`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<String> minuteOptions (default value: const []) | ||||
|     test('to test the property `minuteOptions`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<String> secondOptions (default value: const []) | ||||
|     test('to test the property `secondOptions`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<String> presetOptions (default value: const []) | ||||
|     test('to test the property `presetOptions`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										10
									
								
								notes.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								notes.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| # User defined storage structure | ||||
|  | ||||
| # Folder structure | ||||
| * Year is the top level | ||||
|   * Different parsing sequence will be the second level  | ||||
|  | ||||
| # Filename | ||||
| * Filename will always be appended by a unique ID. Maybe use https://github.com/ai/nanoid | ||||
|   * Example: `notes.md` -> `notes-1234567890.md` | ||||
| * Filename will be unique in the same folder | ||||
| @@ -13,6 +13,7 @@ import { DownloadModule } from '../../modules/download/download.module'; | ||||
| import { TagModule } from '../tag/tag.module'; | ||||
| import { AlbumModule } from '../album/album.module'; | ||||
| import { UserModule } from '../user/user.module'; | ||||
| import { StorageModule } from '@app/storage'; | ||||
|  | ||||
| const ASSET_REPOSITORY_PROVIDER = { | ||||
|   provide: ASSET_REPOSITORY, | ||||
| @@ -28,6 +29,7 @@ const ASSET_REPOSITORY_PROVIDER = { | ||||
|     UserModule, | ||||
|     AlbumModule, | ||||
|     TagModule, | ||||
|     StorageModule, | ||||
|     forwardRef(() => AlbumModule), | ||||
|     BullModule.registerQueue({ | ||||
|       name: QueueNameEnum.ASSET_UPLOADED, | ||||
|   | ||||
| @@ -11,7 +11,8 @@ import { DownloadService } from '../../modules/download/download.service'; | ||||
| import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; | ||||
| import { IAssetUploadedJob, IVideoTranscodeJob } from '@app/job'; | ||||
| import { Queue } from 'bull'; | ||||
| import { IAlbumRepository } from "../album/album-repository"; | ||||
| import { IAlbumRepository } from '../album/album-repository'; | ||||
| import { StorageService } from '@app/storage'; | ||||
|  | ||||
| describe('AssetService', () => { | ||||
|   let sui: AssetService; | ||||
| @@ -22,6 +23,7 @@ describe('AssetService', () => { | ||||
|   let backgroundTaskServiceMock: jest.Mocked<BackgroundTaskService>; | ||||
|   let assetUploadedQueueMock: jest.Mocked<Queue<IAssetUploadedJob>>; | ||||
|   let videoConversionQueueMock: jest.Mocked<Queue<IVideoTranscodeJob>>; | ||||
|   let storageSeriveMock: jest.Mocked<StorageService>; | ||||
|   const authUser: AuthUserDto = Object.freeze({ | ||||
|     id: 'user_id_1', | ||||
|     email: 'auth@test.com', | ||||
| @@ -139,6 +141,7 @@ describe('AssetService', () => { | ||||
|       assetUploadedQueueMock, | ||||
|       videoConversionQueueMock, | ||||
|       downloadServiceMock as DownloadService, | ||||
|       storageSeriveMock, | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   | ||||
| @@ -55,6 +55,7 @@ import { Queue } from 'bull'; | ||||
| import { DownloadService } from '../../modules/download/download.service'; | ||||
| import { DownloadDto } from './dto/download-library.dto'; | ||||
| import { ALBUM_REPOSITORY, IAlbumRepository } from '../album/album-repository'; | ||||
| import { StorageService } from '@app/storage'; | ||||
|  | ||||
| const fileInfo = promisify(stat); | ||||
|  | ||||
| @@ -79,6 +80,8 @@ export class AssetService { | ||||
|     private videoConversionQueue: Queue<IVideoTranscodeJob>, | ||||
|  | ||||
|     private downloadService: DownloadService, | ||||
|  | ||||
|     private storageService: StorageService, | ||||
|   ) {} | ||||
|  | ||||
|   public async handleUploadedAsset( | ||||
| @@ -113,6 +116,8 @@ export class AssetService { | ||||
|           throw new BadRequestException('Asset not created'); | ||||
|         } | ||||
|  | ||||
|         await this.storageService.moveAsset(livePhotoAssetEntity, originalAssetData.originalname); | ||||
|  | ||||
|         await this.videoConversionQueue.add( | ||||
|           mp4ConversionProcessorName, | ||||
|           { asset: livePhotoAssetEntity }, | ||||
| @@ -139,13 +144,15 @@ export class AssetService { | ||||
|         throw new BadRequestException('Asset not created'); | ||||
|       } | ||||
|  | ||||
|       const movedAsset = await this.storageService.moveAsset(assetEntity, originalAssetData.originalname); | ||||
|  | ||||
|       await this.assetUploadedQueue.add( | ||||
|         assetUploadedProcessorName, | ||||
|         { asset: assetEntity, fileName: originalAssetData.originalname }, | ||||
|         { jobId: assetEntity.id }, | ||||
|         { asset: movedAsset, fileName: originalAssetData.originalname }, | ||||
|         { jobId: movedAsset.id }, | ||||
|       ); | ||||
|  | ||||
|       return new AssetFileUploadResponseDto(assetEntity.id); | ||||
|       return new AssetFileUploadResponseDto(movedAsset.id); | ||||
|     } catch (err) { | ||||
|       await this.backgroundTaskService.deleteFileOnDisk([ | ||||
|         { | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| import { IsNotEmpty, IsString } from 'class-validator'; | ||||
|  | ||||
| export class SystemConfigStorageTemplateDto { | ||||
|   @IsNotEmpty() | ||||
|   @IsString() | ||||
|   template!: string; | ||||
| } | ||||
| @@ -2,6 +2,7 @@ import { SystemConfig } from '@app/database/entities/system-config.entity'; | ||||
| import { ValidateNested } from 'class-validator'; | ||||
| import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; | ||||
| import { SystemConfigOAuthDto } from './system-config-oauth.dto'; | ||||
| import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; | ||||
|  | ||||
| export class SystemConfigDto { | ||||
|   @ValidateNested() | ||||
| @@ -9,6 +10,9 @@ export class SystemConfigDto { | ||||
|  | ||||
|   @ValidateNested() | ||||
|   oauth!: SystemConfigOAuthDto; | ||||
|  | ||||
|   @ValidateNested() | ||||
|   storageTemplate!: SystemConfigStorageTemplateDto; | ||||
| } | ||||
|  | ||||
| export function mapConfig(config: SystemConfig): SystemConfigDto { | ||||
|   | ||||
| @@ -0,0 +1,9 @@ | ||||
| export class SystemConfigTemplateStorageOptionDto { | ||||
|   yearOptions!: string[]; | ||||
|   monthOptions!: string[]; | ||||
|   dayOptions!: string[]; | ||||
|   hourOptions!: string[]; | ||||
|   minuteOptions!: string[]; | ||||
|   secondOptions!: string[]; | ||||
|   presetOptions!: string[]; | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common'; | ||||
| import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; | ||||
| import { Authenticated } from '../../decorators/authenticated.decorator'; | ||||
| import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto'; | ||||
| import { SystemConfigDto } from './dto/system-config.dto'; | ||||
| import { SystemConfigService } from './system-config.service'; | ||||
|  | ||||
| @@ -25,4 +26,9 @@ export class SystemConfigController { | ||||
|   public updateConfig(@Body(ValidationPipe) dto: SystemConfigDto): Promise<SystemConfigDto> { | ||||
|     return this.systemConfigService.updateConfig(dto); | ||||
|   } | ||||
|  | ||||
|   @Get('storage-template-options') | ||||
|   public getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { | ||||
|     return this.systemConfigService.getStorageTemplateOptions(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,16 @@ | ||||
| import { | ||||
|   supportedDayTokens, | ||||
|   supportedHourTokens, | ||||
|   supportedMinuteTokens, | ||||
|   supportedMonthTokens, | ||||
|   supportedPresetTokens, | ||||
|   supportedSecondTokens, | ||||
|   supportedYearTokens, | ||||
| } from '@app/storage/constants/supported-datetime-template'; | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { ImmichConfigService } from 'libs/immich-config/src'; | ||||
| import { mapConfig, SystemConfigDto } from './dto/system-config.dto'; | ||||
| import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto'; | ||||
|  | ||||
| @Injectable() | ||||
| export class SystemConfigService { | ||||
| @@ -17,7 +27,21 @@ export class SystemConfigService { | ||||
|   } | ||||
|  | ||||
|   public async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> { | ||||
|     await this.immichConfigService.updateConfig(dto); | ||||
|     return this.getConfig(); | ||||
|     const config = await this.immichConfigService.updateConfig(dto); | ||||
|     return mapConfig(config); | ||||
|   } | ||||
|  | ||||
|   public getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { | ||||
|     const options = new SystemConfigTemplateStorageOptionDto(); | ||||
|  | ||||
|     options.dayOptions = supportedDayTokens; | ||||
|     options.monthOptions = supportedMonthTokens; | ||||
|     options.yearOptions = supportedYearTokens; | ||||
|     options.hourOptions = supportedHourTokens; | ||||
|     options.minuteOptions = supportedMinuteTokens; | ||||
|     options.secondOptions = supportedSecondTokens; | ||||
|     options.presetOptions = supportedPresetTokens; | ||||
|  | ||||
|     return options; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -19,7 +19,7 @@ describe('UserService', () => { | ||||
|     email: 'immich@test.com', | ||||
|   }); | ||||
|  | ||||
|   const adminUser: UserEntity = Object.freeze({ | ||||
|   const adminUser: UserEntity = { | ||||
|     id: 'admin_id', | ||||
|     email: 'admin@test.com', | ||||
|     password: 'admin_password', | ||||
| @@ -32,9 +32,9 @@ describe('UserService', () => { | ||||
|     profileImagePath: '', | ||||
|     createdAt: '2021-01-01', | ||||
|     tags: [], | ||||
|   }); | ||||
|   }; | ||||
|  | ||||
|   const immichUser: UserEntity = Object.freeze({ | ||||
|   const immichUser: UserEntity = { | ||||
|     id: 'immich_id', | ||||
|     email: 'immich@test.com', | ||||
|     password: 'immich_password', | ||||
| @@ -47,9 +47,9 @@ describe('UserService', () => { | ||||
|     profileImagePath: '', | ||||
|     createdAt: '2021-01-01', | ||||
|     tags: [], | ||||
|   }); | ||||
|   }; | ||||
|  | ||||
|   const updatedImmichUser: UserEntity = Object.freeze({ | ||||
|   const updatedImmichUser: UserEntity = { | ||||
|     id: 'immich_id', | ||||
|     email: 'immich@test.com', | ||||
|     password: 'immich_password', | ||||
| @@ -62,7 +62,7 @@ describe('UserService', () => { | ||||
|     profileImagePath: '', | ||||
|     createdAt: '2021-01-01', | ||||
|     tags: [], | ||||
|   }); | ||||
|   }; | ||||
|  | ||||
|   beforeAll(() => { | ||||
|     userRepositoryMock = newUserRepositoryMock(); | ||||
| @@ -75,7 +75,7 @@ describe('UserService', () => { | ||||
|   }); | ||||
|  | ||||
|   describe('Update user', () => { | ||||
|     it('should update user', () => { | ||||
|     it('should update user', async () => { | ||||
|       const requestor = immichAuthUser; | ||||
|       const userToUpdate = immichUser; | ||||
|  | ||||
| @@ -83,11 +83,11 @@ describe('UserService', () => { | ||||
|       userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(userToUpdate)); | ||||
|       userRepositoryMock.update.mockImplementationOnce(() => Promise.resolve(updatedImmichUser)); | ||||
|  | ||||
|       const result = sui.updateUser(requestor, { | ||||
|       const result = await sui.updateUser(requestor, { | ||||
|         id: userToUpdate.id, | ||||
|         shouldChangePassword: true, | ||||
|       }); | ||||
|       expect(result).resolves.toBeDefined(); | ||||
|       expect(result.shouldChangePassword).toEqual(true); | ||||
|     }); | ||||
|  | ||||
|     it('user can only update its information', () => { | ||||
|   | ||||
| @@ -44,6 +44,7 @@ export class ThumbnailGeneratorProcessor { | ||||
|     private configService: ConfigService, | ||||
|   ) { | ||||
|     this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE; | ||||
|     // TODO - Add observable paterrn to listen to the config change | ||||
|   } | ||||
|  | ||||
|   @Process({ name: generateJPEGThumbnailProcessorName, concurrency: 3 }) | ||||
| @@ -59,9 +60,7 @@ export class ThumbnailGeneratorProcessor { | ||||
|       mkdirSync(resizePath, { recursive: true }); | ||||
|     } | ||||
|  | ||||
|     const temp = asset.originalPath.split('/'); | ||||
|     const originalFilename = temp[temp.length - 1].split('.')[0]; | ||||
|     const jpegThumbnailPath = join(resizePath, `${originalFilename}.jpeg`); | ||||
|     const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`); | ||||
|  | ||||
|     if (asset.type == AssetType.IMAGE) { | ||||
|       try { | ||||
|   | ||||
| @@ -2169,12 +2169,38 @@ | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/system-config/storage-template-options": { | ||||
|       "get": { | ||||
|         "operationId": "getStorageTemplateOptions", | ||||
|         "parameters": [], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigTemplateStorageOptionDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "info": { | ||||
|     "title": "Immich", | ||||
|     "description": "Immich API", | ||||
|     "version": "1.38.0", | ||||
|     "version": "1.38.2", | ||||
|     "contact": {} | ||||
|   }, | ||||
|   "tags": [], | ||||
| @@ -3664,6 +3690,17 @@ | ||||
|           "autoRegister" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigStorageTemplateDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "template": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "template" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
| @@ -3672,11 +3709,71 @@ | ||||
|           }, | ||||
|           "oauth": { | ||||
|             "$ref": "#/components/schemas/SystemConfigOAuthDto" | ||||
|           }, | ||||
|           "storageTemplate": { | ||||
|             "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "ffmpeg", | ||||
|           "oauth" | ||||
|           "oauth", | ||||
|           "storageTemplate" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigTemplateStorageOptionDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "yearOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "monthOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "dayOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "hourOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "minuteOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "secondOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "presetOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "yearOptions", | ||||
|           "monthOptions", | ||||
|           "dayOptions", | ||||
|           "hourOptions", | ||||
|           "minuteOptions", | ||||
|           "secondOptions", | ||||
|           "presetOptions" | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -25,6 +25,7 @@ export enum SystemConfigKey { | ||||
|   OAUTH_SCOPE = 'oauth.scope', | ||||
|   OAUTH_BUTTON_TEXT = 'oauth.buttonText', | ||||
|   OAUTH_AUTO_REGISTER = 'oauth.autoRegister', | ||||
|   STORAGE_TEMPLATE = 'storageTemplate.template', | ||||
| } | ||||
|  | ||||
| export interface SystemConfig { | ||||
| @@ -44,4 +45,7 @@ export interface SystemConfig { | ||||
|     buttonText: string; | ||||
|     autoRegister: boolean; | ||||
|   }; | ||||
|   storageTemplate: { | ||||
|     template: string; | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,24 @@ | ||||
| import { SystemConfigEntity } from '@app/database/entities/system-config.entity'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { Module, Provider } from '@nestjs/common'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { ImmichConfigService } from './immich-config.service'; | ||||
|  | ||||
| export const INITIAL_SYSTEM_CONFIG = 'INITIAL_SYSTEM_CONFIG'; | ||||
|  | ||||
| const providers: Provider[] = [ | ||||
|   ImmichConfigService, | ||||
|   { | ||||
|     provide: INITIAL_SYSTEM_CONFIG, | ||||
|     inject: [ImmichConfigService], | ||||
|     useFactory: async (configService: ImmichConfigService) => { | ||||
|       return configService.getConfig(); | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [TypeOrmModule.forFeature([SystemConfigEntity])], | ||||
|   providers: [ImmichConfigService], | ||||
|   exports: [ImmichConfigService], | ||||
|   providers: [...providers], | ||||
|   exports: [...providers], | ||||
| }) | ||||
| export class ImmichConfigModule {} | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import { SystemConfig, SystemConfigEntity, SystemConfigKey } from '@app/database/entities/system-config.entity'; | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { BadRequestException, Injectable, Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import * as _ from 'lodash'; | ||||
| import { Subject } from 'rxjs'; | ||||
| import { DeepPartial, In, Repository } from 'typeorm'; | ||||
|  | ||||
| export type SystemConfigValidator = (config: SystemConfig) => void | Promise<void>; | ||||
|  | ||||
| const defaults: SystemConfig = Object.freeze({ | ||||
|   ffmpeg: { | ||||
|     crf: '23', | ||||
| @@ -21,10 +24,19 @@ const defaults: SystemConfig = Object.freeze({ | ||||
|     buttonText: 'Login with OAuth', | ||||
|     autoRegister: true, | ||||
|   }, | ||||
|  | ||||
|   storageTemplate: { | ||||
|     template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| @Injectable() | ||||
| export class ImmichConfigService { | ||||
|   private logger = new Logger(ImmichConfigService.name); | ||||
|   private validators: SystemConfigValidator[] = []; | ||||
|  | ||||
|   public config$ = new Subject<SystemConfig>(); | ||||
|  | ||||
|   constructor( | ||||
|     @InjectRepository(SystemConfigEntity) | ||||
|     private systemConfigRepository: Repository<SystemConfigEntity>, | ||||
| @@ -34,6 +46,10 @@ export class ImmichConfigService { | ||||
|     return defaults; | ||||
|   } | ||||
|  | ||||
|   public addValidator(validator: SystemConfigValidator) { | ||||
|     this.validators.push(validator); | ||||
|   } | ||||
|  | ||||
|   public async getConfig() { | ||||
|     const overrides = await this.systemConfigRepository.find(); | ||||
|     const config: DeepPartial<SystemConfig> = {}; | ||||
| @@ -45,7 +61,16 @@ export class ImmichConfigService { | ||||
|     return _.defaultsDeep(config, defaults) as SystemConfig; | ||||
|   } | ||||
|  | ||||
|   public async updateConfig(config: DeepPartial<SystemConfig> | null): Promise<void> { | ||||
|   public async updateConfig(config: SystemConfig): Promise<SystemConfig> { | ||||
|     try { | ||||
|       for (const validator of this.validators) { | ||||
|         await validator(config); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.logger.warn(`Unable to save system config due to a validation error: ${e}`); | ||||
|       throw new BadRequestException(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|  | ||||
|     const updates: SystemConfigEntity[] = []; | ||||
|     const deletes: SystemConfigEntity[] = []; | ||||
|  | ||||
| @@ -70,5 +95,11 @@ export class ImmichConfigService { | ||||
|     if (deletes.length > 0) { | ||||
|       await this.systemConfigRepository.delete({ key: In(deletes.map((item) => item.key)) }); | ||||
|     } | ||||
|  | ||||
|     const newConfig = await this.getConfig(); | ||||
|  | ||||
|     this.config$.next(newConfig); | ||||
|  | ||||
|     return newConfig; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,20 @@ | ||||
| export const supportedYearTokens = ['y', 'yy']; | ||||
| export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM']; | ||||
| export const supportedDayTokens = ['d', 'dd']; | ||||
| export const supportedHourTokens = ['h', 'hh', 'H', 'HH']; | ||||
| export const supportedMinuteTokens = ['m', 'mm']; | ||||
| export const supportedSecondTokens = ['s', 'ss']; | ||||
| export const supportedPresetTokens = [ | ||||
|   '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|   '{{y}}/{{MM}}-{{dd}}/{{filename}}', | ||||
|   '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', | ||||
|   '{{y}}/{{MM}}/{{filename}}', | ||||
|   '{{y}}/{{MMM}}/{{filename}}', | ||||
|   '{{y}}/{{MMMM}}/{{filename}}', | ||||
|   '{{y}}/{{MM}}/{{dd}}/{{filename}}', | ||||
|   '{{y}}/{{MMMM}}/{{dd}}/{{filename}}', | ||||
|   '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|   '{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|   '{{y}}-{{MMM}}-{{dd}}/{{filename}}', | ||||
|   '{{y}}-{{MMMM}}-{{dd}}/{{filename}}', | ||||
| ]; | ||||
							
								
								
									
										2
									
								
								server/libs/storage/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								server/libs/storage/src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export * from './storage.module'; | ||||
| export * from './storage.service'; | ||||
| @@ -0,0 +1,6 @@ | ||||
| export interface IImmichStorage { | ||||
|   write(): Promise<void>; | ||||
|   read(): Promise<void>; | ||||
| } | ||||
|  | ||||
| export enum IStorageType {} | ||||
							
								
								
									
										13
									
								
								server/libs/storage/src/storage.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								server/libs/storage/src/storage.module.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| import { SystemConfigEntity } from '@app/database/entities/system-config.entity'; | ||||
| import { ImmichConfigModule } from '@app/immich-config'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { StorageService } from './storage.service'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [TypeOrmModule.forFeature([AssetEntity, SystemConfigEntity]), ImmichConfigModule], | ||||
|   providers: [StorageService], | ||||
|   exports: [StorageService], | ||||
| }) | ||||
| export class StorageModule {} | ||||
							
								
								
									
										153
									
								
								server/libs/storage/src/storage.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								server/libs/storage/src/storage.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| import { APP_UPLOAD_LOCATION } from '@app/common'; | ||||
| import { AssetEntity } from '@app/database/entities/asset.entity'; | ||||
| import { SystemConfig } from '@app/database/entities/system-config.entity'; | ||||
| import { ImmichConfigService, INITIAL_SYSTEM_CONFIG } from '@app/immich-config'; | ||||
| import { Inject, Injectable, Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import fsPromise from 'fs/promises'; | ||||
| import handlebar from 'handlebars'; | ||||
| import * as luxon from 'luxon'; | ||||
| import mv from 'mv'; | ||||
| import { constants } from 'node:fs'; | ||||
| import path from 'node:path'; | ||||
| import { promisify } from 'node:util'; | ||||
| import sanitize from 'sanitize-filename'; | ||||
| import { Repository } from 'typeorm'; | ||||
| import { | ||||
|   supportedDayTokens, | ||||
|   supportedHourTokens, | ||||
|   supportedMinuteTokens, | ||||
|   supportedMonthTokens, | ||||
|   supportedSecondTokens, | ||||
|   supportedYearTokens, | ||||
| } from './constants/supported-datetime-template'; | ||||
|  | ||||
| const moveFile = promisify<string, string, mv.Options>(mv); | ||||
|  | ||||
| @Injectable() | ||||
| export class StorageService { | ||||
|   readonly log = new Logger(StorageService.name); | ||||
|  | ||||
|   private storageTemplate: HandlebarsTemplateDelegate<any>; | ||||
|  | ||||
|   constructor( | ||||
|     @InjectRepository(AssetEntity) | ||||
|     private assetRepository: Repository<AssetEntity>, | ||||
|     private immichConfigService: ImmichConfigService, | ||||
|     @Inject(INITIAL_SYSTEM_CONFIG) config: SystemConfig, | ||||
|   ) { | ||||
|     this.storageTemplate = this.compile(config.storageTemplate.template); | ||||
|  | ||||
|     this.immichConfigService.addValidator((config) => this.validateConfig(config)); | ||||
|  | ||||
|     this.immichConfigService.config$.subscribe((config) => { | ||||
|       this.log.debug(`Received new config, recompiling storage template: ${config.storageTemplate.template}`); | ||||
|       this.storageTemplate = this.compile(config.storageTemplate.template); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   public async moveAsset(asset: AssetEntity, filename: string): Promise<AssetEntity> { | ||||
|     try { | ||||
|       const source = asset.originalPath; | ||||
|       const ext = path.extname(source).split('.').pop() as string; | ||||
|       const sanitized = sanitize(path.basename(filename, `.${ext}`)); | ||||
|       const rootPath = path.join(APP_UPLOAD_LOCATION, asset.userId); | ||||
|       const storagePath = this.render(this.storageTemplate, asset, sanitized, ext); | ||||
|       const fullPath = path.normalize(path.join(rootPath, storagePath)); | ||||
|  | ||||
|       if (!fullPath.startsWith(rootPath)) { | ||||
|         this.log.warn(`Skipped attempt to access an invalid path: ${fullPath}. Path should start with ${rootPath}`); | ||||
|         return asset; | ||||
|       } | ||||
|  | ||||
|       let duplicateCount = 0; | ||||
|       let destination = `${fullPath}.${ext}`; | ||||
|  | ||||
|       while (true) { | ||||
|         const exists = await this.checkFileExist(destination); | ||||
|         if (!exists) { | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         duplicateCount++; | ||||
|         destination = `${fullPath}_${duplicateCount}.${ext}`; | ||||
|       } | ||||
|  | ||||
|       await this.safeMove(source, destination); | ||||
|  | ||||
|       asset.originalPath = destination; | ||||
|       return await this.assetRepository.save(asset); | ||||
|     } catch (error: any) { | ||||
|       this.log.error(error, error.stack); | ||||
|       return asset; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private safeMove(source: string, destination: string): Promise<void> { | ||||
|     return moveFile(source, destination, { mkdirp: true, clobber: false }); | ||||
|   } | ||||
|  | ||||
|   private async checkFileExist(path: string): Promise<boolean> { | ||||
|     try { | ||||
|       await fsPromise.access(path, constants.F_OK); | ||||
|       return true; | ||||
|     } catch (_) { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private validateConfig(config: SystemConfig) { | ||||
|     this.validateStorageTemplate(config.storageTemplate.template); | ||||
|   } | ||||
|  | ||||
|   private validateStorageTemplate(templateString: string) { | ||||
|     try { | ||||
|       const template = this.compile(templateString); | ||||
|  | ||||
|       // test render an asset | ||||
|       this.render( | ||||
|         template, | ||||
|         { | ||||
|           createdAt: new Date().toISOString(), | ||||
|           originalPath: '/upload/test/IMG_123.jpg', | ||||
|         } as AssetEntity, | ||||
|         'IMG_123', | ||||
|         'jpg', | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       this.log.warn(`Storage template validation failed: ${e}`); | ||||
|       throw new Error(`Invalid storage template: ${e}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private compile(template: string) { | ||||
|     return handlebar.compile(template, { | ||||
|       knownHelpers: undefined, | ||||
|       strict: true, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private render(template: HandlebarsTemplateDelegate<any>, asset: AssetEntity, filename: string, ext: string) { | ||||
|     const substitutions: Record<string, string> = { | ||||
|       filename, | ||||
|       ext, | ||||
|     }; | ||||
|  | ||||
|     const dt = luxon.DateTime.fromISO(new Date(asset.createdAt).toISOString()); | ||||
|  | ||||
|     const dateTokens = [ | ||||
|       ...supportedYearTokens, | ||||
|       ...supportedMonthTokens, | ||||
|       ...supportedDayTokens, | ||||
|       ...supportedHourTokens, | ||||
|       ...supportedMinuteTokens, | ||||
|       ...supportedSecondTokens, | ||||
|     ]; | ||||
|  | ||||
|     for (const token of dateTokens) { | ||||
|       substitutions[token] = dt.toFormat(token); | ||||
|     } | ||||
|  | ||||
|     return template(substitutions); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								server/libs/storage/tsconfig.lib.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/libs/storage/tsconfig.lib.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "extends": "../../tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "declaration": true, | ||||
|     "outDir": "../../dist/libs/storage" | ||||
|   }, | ||||
|   "include": ["src/**/*"], | ||||
|   "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] | ||||
| } | ||||
| @@ -79,6 +79,15 @@ | ||||
|       "compilerOptions": { | ||||
|         "tsConfigPath": "libs/immich-config/tsconfig.lib.json" | ||||
|       } | ||||
|     }, | ||||
|     "storage": { | ||||
|       "type": "library", | ||||
|       "root": "libs/storage", | ||||
|       "entryFile": "index", | ||||
|       "sourceRoot": "libs/storage/src", | ||||
|       "compilerOptions": { | ||||
|         "tsConfigPath": "libs/storage/tsconfig.lib.json" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										180
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										180
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -36,11 +36,13 @@ | ||||
|         "fdir": "^5.3.0", | ||||
|         "fluent-ffmpeg": "^2.1.2", | ||||
|         "geo-tz": "^7.0.2", | ||||
|         "handlebars": "^4.7.7", | ||||
|         "i18n-iso-countries": "^7.5.0", | ||||
|         "joi": "^17.5.0", | ||||
|         "local-reverse-geocoder": "^0.12.5", | ||||
|         "lodash": "^4.17.21", | ||||
|         "luxon": "^3.0.3", | ||||
|         "mv": "^2.1.1", | ||||
|         "nest-commander": "^3.3.0", | ||||
|         "openid-client": "^5.2.1", | ||||
|         "passport": "^0.6.0", | ||||
| @@ -76,6 +78,7 @@ | ||||
|         "@types/jest": "27.0.2", | ||||
|         "@types/lodash": "^4.14.178", | ||||
|         "@types/multer": "^1.4.7", | ||||
|         "@types/mv": "^2.1.2", | ||||
|         "@types/node": "^16.0.0", | ||||
|         "@types/passport-jwt": "^3.0.6", | ||||
|         "@types/sharp": "^0.30.2", | ||||
| @@ -2544,6 +2547,12 @@ | ||||
|         "@types/express": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/mv": { | ||||
|       "version": "2.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/@types/mv/-/mv-2.1.2.tgz", | ||||
|       "integrity": "sha512-IvAjPuiQ2exDicnTrMidt1m+tj3gZ60BM0PaoRsU0m9Cn+lrOyemuO9Tf8CvHFmXlxMjr1TVCfadi9sfwbSuKg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/@types/node": { | ||||
|       "version": "16.11.21", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz", | ||||
| @@ -6168,6 +6177,34 @@ | ||||
|       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", | ||||
|       "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" | ||||
|     }, | ||||
|     "node_modules/handlebars": { | ||||
|       "version": "4.7.7", | ||||
|       "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", | ||||
|       "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", | ||||
|       "dependencies": { | ||||
|         "minimist": "^1.2.5", | ||||
|         "neo-async": "^2.6.0", | ||||
|         "source-map": "^0.6.1", | ||||
|         "wordwrap": "^1.0.0" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "handlebars": "bin/handlebars" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.4.7" | ||||
|       }, | ||||
|       "optionalDependencies": { | ||||
|         "uglify-js": "^3.1.4" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/handlebars/node_modules/source-map": { | ||||
|       "version": "0.6.1", | ||||
|       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | ||||
|       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", | ||||
|       "engines": { | ||||
|         "node": ">=0.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/har-schema": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", | ||||
| @@ -8178,6 +8215,45 @@ | ||||
|       "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", | ||||
|       "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" | ||||
|     }, | ||||
|     "node_modules/mv": { | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", | ||||
|       "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", | ||||
|       "dependencies": { | ||||
|         "mkdirp": "~0.5.1", | ||||
|         "ncp": "~2.0.0", | ||||
|         "rimraf": "~2.4.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.8.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/mv/node_modules/glob": { | ||||
|       "version": "6.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", | ||||
|       "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", | ||||
|       "dependencies": { | ||||
|         "inflight": "^1.0.4", | ||||
|         "inherits": "2", | ||||
|         "minimatch": "2 || 3", | ||||
|         "once": "^1.3.0", | ||||
|         "path-is-absolute": "^1.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/mv/node_modules/rimraf": { | ||||
|       "version": "2.4.5", | ||||
|       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", | ||||
|       "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", | ||||
|       "dependencies": { | ||||
|         "glob": "^6.0.1" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "rimraf": "bin.js" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/mz": { | ||||
|       "version": "2.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", | ||||
| @@ -8204,6 +8280,14 @@ | ||||
|       "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/ncp": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", | ||||
|       "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", | ||||
|       "bin": { | ||||
|         "ncp": "bin/ncp" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/negotiator": { | ||||
|       "version": "0.6.3", | ||||
|       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", | ||||
| @@ -8215,8 +8299,7 @@ | ||||
|     "node_modules/neo-async": { | ||||
|       "version": "2.6.2", | ||||
|       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", | ||||
|       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" | ||||
|     }, | ||||
|     "node_modules/nest-commander": { | ||||
|       "version": "3.3.0", | ||||
| @@ -11006,6 +11089,18 @@ | ||||
|         "node": ">=4.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/uglify-js": { | ||||
|       "version": "3.17.4", | ||||
|       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", | ||||
|       "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", | ||||
|       "optional": true, | ||||
|       "bin": { | ||||
|         "uglifyjs": "bin/uglifyjs" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.8.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/uid2": { | ||||
|       "version": "0.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", | ||||
| @@ -11329,6 +11424,11 @@ | ||||
|         "node": ">=0.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/wordwrap": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", | ||||
|       "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" | ||||
|     }, | ||||
|     "node_modules/wrap-ansi": { | ||||
|       "version": "7.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", | ||||
| @@ -13393,6 +13493,12 @@ | ||||
|         "@types/express": "*" | ||||
|       } | ||||
|     }, | ||||
|     "@types/mv": { | ||||
|       "version": "2.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/@types/mv/-/mv-2.1.2.tgz", | ||||
|       "integrity": "sha512-IvAjPuiQ2exDicnTrMidt1m+tj3gZ60BM0PaoRsU0m9Cn+lrOyemuO9Tf8CvHFmXlxMjr1TVCfadi9sfwbSuKg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@types/node": { | ||||
|       "version": "16.11.21", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz", | ||||
| @@ -16213,6 +16319,25 @@ | ||||
|       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", | ||||
|       "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" | ||||
|     }, | ||||
|     "handlebars": { | ||||
|       "version": "4.7.7", | ||||
|       "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", | ||||
|       "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", | ||||
|       "requires": { | ||||
|         "minimist": "^1.2.5", | ||||
|         "neo-async": "^2.6.0", | ||||
|         "source-map": "^0.6.1", | ||||
|         "uglify-js": "^3.1.4", | ||||
|         "wordwrap": "^1.0.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "source-map": { | ||||
|           "version": "0.6.1", | ||||
|           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | ||||
|           "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "har-schema": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", | ||||
| @@ -17773,6 +17898,38 @@ | ||||
|       "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", | ||||
|       "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" | ||||
|     }, | ||||
|     "mv": { | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", | ||||
|       "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", | ||||
|       "requires": { | ||||
|         "mkdirp": "~0.5.1", | ||||
|         "ncp": "~2.0.0", | ||||
|         "rimraf": "~2.4.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "glob": { | ||||
|           "version": "6.0.4", | ||||
|           "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", | ||||
|           "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", | ||||
|           "requires": { | ||||
|             "inflight": "^1.0.4", | ||||
|             "inherits": "2", | ||||
|             "minimatch": "2 || 3", | ||||
|             "once": "^1.3.0", | ||||
|             "path-is-absolute": "^1.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "rimraf": { | ||||
|           "version": "2.4.5", | ||||
|           "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", | ||||
|           "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", | ||||
|           "requires": { | ||||
|             "glob": "^6.0.1" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "mz": { | ||||
|       "version": "2.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", | ||||
| @@ -17799,6 +17956,11 @@ | ||||
|       "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "ncp": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", | ||||
|       "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==" | ||||
|     }, | ||||
|     "negotiator": { | ||||
|       "version": "0.6.3", | ||||
|       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", | ||||
| @@ -17807,8 +17969,7 @@ | ||||
|     "neo-async": { | ||||
|       "version": "2.6.2", | ||||
|       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", | ||||
|       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" | ||||
|     }, | ||||
|     "nest-commander": { | ||||
|       "version": "3.3.0", | ||||
| @@ -19794,6 +19955,12 @@ | ||||
|       "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", | ||||
|       "devOptional": true | ||||
|     }, | ||||
|     "uglify-js": { | ||||
|       "version": "3.17.4", | ||||
|       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", | ||||
|       "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "uid2": { | ||||
|       "version": "0.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", | ||||
| @@ -20049,6 +20216,11 @@ | ||||
|       "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "wordwrap": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", | ||||
|       "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" | ||||
|     }, | ||||
|     "wrap-ansi": { | ||||
|       "version": "7.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", | ||||
|   | ||||
| @@ -59,11 +59,13 @@ | ||||
|     "fdir": "^5.3.0", | ||||
|     "fluent-ffmpeg": "^2.1.2", | ||||
|     "geo-tz": "^7.0.2", | ||||
|     "handlebars": "^4.7.7", | ||||
|     "i18n-iso-countries": "^7.5.0", | ||||
|     "joi": "^17.5.0", | ||||
|     "local-reverse-geocoder": "^0.12.5", | ||||
|     "lodash": "^4.17.21", | ||||
|     "luxon": "^3.0.3", | ||||
|     "mv": "^2.1.1", | ||||
|     "nest-commander": "^3.3.0", | ||||
|     "openid-client": "^5.2.1", | ||||
|     "passport": "^0.6.0", | ||||
| @@ -96,6 +98,7 @@ | ||||
|     "@types/jest": "27.0.2", | ||||
|     "@types/lodash": "^4.14.178", | ||||
|     "@types/multer": "^1.4.7", | ||||
|     "@types/mv": "^2.1.2", | ||||
|     "@types/node": "^16.0.0", | ||||
|     "@types/passport-jwt": "^3.0.6", | ||||
|     "@types/sharp": "^0.30.2", | ||||
| @@ -142,7 +145,8 @@ | ||||
|       "@app/database/config": "<rootDir>/libs/database/src/config", | ||||
|       "@app/common": "<rootDir>/libs/common/src", | ||||
|       "^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1", | ||||
|       "^@app/immich-config(|/.*)$": "<rootDir>/libs/immich-config/src/$1" | ||||
|       "^@app/immich-config(|/.*)$": "<rootDir>/libs/immich-config/src/$1", | ||||
|       "^@app/storage(|/.*)$": "<rootDir>/libs/storage/src/$1" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -16,15 +16,41 @@ | ||||
|     "esModuleInterop": true, | ||||
|     "baseUrl": "./", | ||||
|     "paths": { | ||||
|       "@app/common": ["libs/common/src"], | ||||
|       "@app/common/*": ["libs/common/src/*"], | ||||
|       "@app/database": ["libs/database/src"], | ||||
|       "@app/database/*": ["libs/database/src/*"], | ||||
|       "@app/job": ["libs/job/src"], | ||||
|       "@app/job/*": ["libs/job/src/*"], | ||||
|       "@app/immich-config": ["libs/immich-config/src"], | ||||
|       "@app/immich-config/*": ["libs/immich-config/src/*"] | ||||
|       "@app/common": [ | ||||
|         "libs/common/src" | ||||
|       ], | ||||
|       "@app/common/*": [ | ||||
|         "libs/common/src/*" | ||||
|       ], | ||||
|       "@app/database": [ | ||||
|         "libs/database/src" | ||||
|       ], | ||||
|       "@app/database/*": [ | ||||
|         "libs/database/src/*" | ||||
|       ], | ||||
|       "@app/job": [ | ||||
|         "libs/job/src" | ||||
|       ], | ||||
|       "@app/job/*": [ | ||||
|         "libs/job/src/*" | ||||
|       ], | ||||
|       "@app/immich-config": [ | ||||
|         "libs/immich-config/src" | ||||
|       ], | ||||
|       "@app/immich-config/*": [ | ||||
|         "libs/immich-config/src/*" | ||||
|       ], | ||||
|       "@app/storage": [ | ||||
|         "libs/storage/src" | ||||
|       ], | ||||
|       "@app/storage/*": [ | ||||
|         "libs/storage/src/*" | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   "exclude": ["dist", "node_modules", "upload"] | ||||
|   "exclude": [ | ||||
|     "dist", | ||||
|     "node_modules", | ||||
|     "upload" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										106
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										106
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -12,9 +12,11 @@ | ||||
| 				"cookie": "^0.4.2", | ||||
| 				"copy-image-clipboard": "^2.1.2", | ||||
| 				"exifr": "^7.1.3", | ||||
| 				"handlebars": "^4.7.7", | ||||
| 				"leaflet": "^1.8.0", | ||||
| 				"lodash": "^4.17.21", | ||||
| 				"lodash-es": "^4.17.21", | ||||
| 				"luxon": "^3.1.1", | ||||
| 				"socket.io-client": "^4.5.1", | ||||
| 				"svelte-keydown": "^0.5.0", | ||||
| 				"svelte-material-icons": "^2.0.2" | ||||
| @@ -34,6 +36,7 @@ | ||||
| 				"@types/leaflet": "^1.7.10", | ||||
| 				"@types/lodash": "^4.14.182", | ||||
| 				"@types/lodash-es": "^4.17.6", | ||||
| 				"@types/luxon": "^3.1.0", | ||||
| 				"@types/socket.io-client": "^3.0.0", | ||||
| 				"@typescript-eslint/eslint-plugin": "^5.27.0", | ||||
| 				"@typescript-eslint/parser": "^5.27.0", | ||||
| @@ -3319,6 +3322,12 @@ | ||||
| 				"@types/lodash": "*" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@types/luxon": { | ||||
| 			"version": "3.1.0", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.1.0.tgz", | ||||
| 			"integrity": "sha512-gCd/HcCgjqSxfMrgtqxCgYk/22NBQfypwFUG7ZAyG/4pqs51WLTcUzVp1hqTbieDYeHS3WoVEh2Yv/2l+7B0Vg==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"node_modules/@types/node": { | ||||
| 			"version": "18.11.11", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.11.tgz", | ||||
| @@ -6149,6 +6158,26 @@ | ||||
| 			"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"node_modules/handlebars": { | ||||
| 			"version": "4.7.7", | ||||
| 			"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", | ||||
| 			"integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", | ||||
| 			"dependencies": { | ||||
| 				"minimist": "^1.2.5", | ||||
| 				"neo-async": "^2.6.0", | ||||
| 				"source-map": "^0.6.1", | ||||
| 				"wordwrap": "^1.0.0" | ||||
| 			}, | ||||
| 			"bin": { | ||||
| 				"handlebars": "bin/handlebars" | ||||
| 			}, | ||||
| 			"engines": { | ||||
| 				"node": ">=0.4.7" | ||||
| 			}, | ||||
| 			"optionalDependencies": { | ||||
| 				"uglify-js": "^3.1.4" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/has": { | ||||
| 			"version": "1.0.3", | ||||
| 			"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", | ||||
| @@ -8976,6 +9005,14 @@ | ||||
| 				"node": ">=10" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/luxon": { | ||||
| 			"version": "3.1.1", | ||||
| 			"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz", | ||||
| 			"integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==", | ||||
| 			"engines": { | ||||
| 				"node": ">=12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/lz-string": { | ||||
| 			"version": "1.4.4", | ||||
| 			"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", | ||||
| @@ -9114,7 +9151,6 @@ | ||||
| 			"version": "1.2.7", | ||||
| 			"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", | ||||
| 			"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", | ||||
| 			"dev": true, | ||||
| 			"funding": { | ||||
| 				"url": "https://github.com/sponsors/ljharb" | ||||
| 			} | ||||
| @@ -9178,6 +9214,11 @@ | ||||
| 			"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"node_modules/neo-async": { | ||||
| 			"version": "2.6.2", | ||||
| 			"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", | ||||
| 			"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" | ||||
| 		}, | ||||
| 		"node_modules/node-int64": { | ||||
| 			"version": "0.4.0", | ||||
| 			"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", | ||||
| @@ -10280,7 +10321,6 @@ | ||||
| 			"version": "0.6.1", | ||||
| 			"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | ||||
| 			"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", | ||||
| 			"dev": true, | ||||
| 			"engines": { | ||||
| 				"node": ">=0.10.0" | ||||
| 			} | ||||
| @@ -10835,6 +10875,18 @@ | ||||
| 				"node": ">=4.2.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/uglify-js": { | ||||
| 			"version": "3.17.4", | ||||
| 			"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", | ||||
| 			"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", | ||||
| 			"optional": true, | ||||
| 			"bin": { | ||||
| 				"uglifyjs": "bin/uglifyjs" | ||||
| 			}, | ||||
| 			"engines": { | ||||
| 				"node": ">=0.8.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/undici": { | ||||
| 			"version": "5.13.0", | ||||
| 			"resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz", | ||||
| @@ -11163,6 +11215,11 @@ | ||||
| 				"node": ">=0.10.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/wordwrap": { | ||||
| 			"version": "1.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", | ||||
| 			"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" | ||||
| 		}, | ||||
| 		"node_modules/wrap-ansi": { | ||||
| 			"version": "7.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", | ||||
| @@ -13726,6 +13783,12 @@ | ||||
| 				"@types/lodash": "*" | ||||
| 			} | ||||
| 		}, | ||||
| 		"@types/luxon": { | ||||
| 			"version": "3.1.0", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.1.0.tgz", | ||||
| 			"integrity": "sha512-gCd/HcCgjqSxfMrgtqxCgYk/22NBQfypwFUG7ZAyG/4pqs51WLTcUzVp1hqTbieDYeHS3WoVEh2Yv/2l+7B0Vg==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"@types/node": { | ||||
| 			"version": "18.11.11", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.11.tgz", | ||||
| @@ -15703,6 +15766,18 @@ | ||||
| 			"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"handlebars": { | ||||
| 			"version": "4.7.7", | ||||
| 			"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", | ||||
| 			"integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", | ||||
| 			"requires": { | ||||
| 				"minimist": "^1.2.5", | ||||
| 				"neo-async": "^2.6.0", | ||||
| 				"source-map": "^0.6.1", | ||||
| 				"uglify-js": "^3.1.4", | ||||
| 				"wordwrap": "^1.0.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"has": { | ||||
| 			"version": "1.0.3", | ||||
| 			"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", | ||||
| @@ -17789,6 +17864,11 @@ | ||||
| 				"yallist": "^4.0.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"luxon": { | ||||
| 			"version": "3.1.1", | ||||
| 			"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz", | ||||
| 			"integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==" | ||||
| 		}, | ||||
| 		"lz-string": { | ||||
| 			"version": "1.4.4", | ||||
| 			"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", | ||||
| @@ -17887,8 +17967,7 @@ | ||||
| 		"minimist": { | ||||
| 			"version": "1.2.7", | ||||
| 			"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", | ||||
| 			"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", | ||||
| 			"dev": true | ||||
| 			"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" | ||||
| 		}, | ||||
| 		"mkdirp": { | ||||
| 			"version": "0.5.6", | ||||
| @@ -17934,6 +18013,11 @@ | ||||
| 			"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"neo-async": { | ||||
| 			"version": "2.6.2", | ||||
| 			"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", | ||||
| 			"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" | ||||
| 		}, | ||||
| 		"node-int64": { | ||||
| 			"version": "0.4.0", | ||||
| 			"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", | ||||
| @@ -18708,8 +18792,7 @@ | ||||
| 		"source-map": { | ||||
| 			"version": "0.6.1", | ||||
| 			"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | ||||
| 			"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", | ||||
| 			"dev": true | ||||
| 			"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" | ||||
| 		}, | ||||
| 		"source-map-js": { | ||||
| 			"version": "1.0.2", | ||||
| @@ -19092,6 +19175,12 @@ | ||||
| 			"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"uglify-js": { | ||||
| 			"version": "3.17.4", | ||||
| 			"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", | ||||
| 			"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", | ||||
| 			"optional": true | ||||
| 		}, | ||||
| 		"undici": { | ||||
| 			"version": "5.13.0", | ||||
| 			"resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz", | ||||
| @@ -19304,6 +19393,11 @@ | ||||
| 			"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"wordwrap": { | ||||
| 			"version": "1.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", | ||||
| 			"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" | ||||
| 		}, | ||||
| 		"wrap-ansi": { | ||||
| 			"version": "7.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", | ||||
|   | ||||
| @@ -32,6 +32,7 @@ | ||||
| 		"@types/leaflet": "^1.7.10", | ||||
| 		"@types/lodash": "^4.14.182", | ||||
| 		"@types/lodash-es": "^4.17.6", | ||||
| 		"@types/luxon": "^3.1.0", | ||||
| 		"@types/socket.io-client": "^3.0.0", | ||||
| 		"@typescript-eslint/eslint-plugin": "^5.27.0", | ||||
| 		"@typescript-eslint/parser": "^5.27.0", | ||||
| @@ -62,9 +63,11 @@ | ||||
| 		"cookie": "^0.4.2", | ||||
| 		"copy-image-clipboard": "^2.1.2", | ||||
| 		"exifr": "^7.1.3", | ||||
| 		"handlebars": "^4.7.7", | ||||
| 		"leaflet": "^1.8.0", | ||||
| 		"lodash": "^4.17.21", | ||||
| 		"lodash-es": "^4.17.21", | ||||
| 		"luxon": "^3.1.1", | ||||
| 		"socket.io-client": "^4.5.1", | ||||
| 		"svelte-keydown": "^0.5.0", | ||||
| 		"svelte-material-icons": "^2.0.2" | ||||
|   | ||||
							
								
								
									
										130
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										130
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.0 | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
| @@ -1443,6 +1443,12 @@ export interface SystemConfigDto { | ||||
|      * @memberof SystemConfigDto | ||||
|      */ | ||||
|     'oauth': SystemConfigOAuthDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {SystemConfigStorageTemplateDto} | ||||
|      * @memberof SystemConfigDto | ||||
|      */ | ||||
|     'storageTemplate': SystemConfigStorageTemplateDto; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -1530,6 +1536,68 @@ export interface SystemConfigOAuthDto { | ||||
|      */ | ||||
|     'autoRegister': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface SystemConfigStorageTemplateDto | ||||
|  */ | ||||
| export interface SystemConfigStorageTemplateDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof SystemConfigStorageTemplateDto | ||||
|      */ | ||||
|     'template': string; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface SystemConfigTemplateStorageOptionDto | ||||
|  */ | ||||
| export interface SystemConfigTemplateStorageOptionDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof SystemConfigTemplateStorageOptionDto | ||||
|      */ | ||||
|     'yearOptions': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof SystemConfigTemplateStorageOptionDto | ||||
|      */ | ||||
|     'monthOptions': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof SystemConfigTemplateStorageOptionDto | ||||
|      */ | ||||
|     'dayOptions': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof SystemConfigTemplateStorageOptionDto | ||||
|      */ | ||||
|     'hourOptions': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof SystemConfigTemplateStorageOptionDto | ||||
|      */ | ||||
|     'minuteOptions': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof SystemConfigTemplateStorageOptionDto | ||||
|      */ | ||||
|     'secondOptions': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof SystemConfigTemplateStorageOptionDto | ||||
|      */ | ||||
|     'presetOptions': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -5312,6 +5380,39 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getStorageTemplateOptions: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             const localVarPath = `/system-config/storage-template-options`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
| 
 | ||||
|             const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
| 
 | ||||
|             // authentication bearer required
 | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
| @@ -5388,6 +5489,15 @@ export const SystemConfigApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getDefaults(options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getStorageTemplateOptions(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigTemplateStorageOptionDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getStorageTemplateOptions(options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {SystemConfigDto} systemConfigDto  | ||||
| @@ -5424,6 +5534,14 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b | ||||
|         getDefaults(options?: any): AxiosPromise<SystemConfigDto> { | ||||
|             return localVarFp.getDefaults(options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getStorageTemplateOptions(options?: any): AxiosPromise<SystemConfigTemplateStorageOptionDto> { | ||||
|             return localVarFp.getStorageTemplateOptions(options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {SystemConfigDto} systemConfigDto  | ||||
| @@ -5463,6 +5581,16 @@ export class SystemConfigApi extends BaseAPI { | ||||
|         return SystemConfigApiFp(this.configuration).getDefaults(options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof SystemConfigApi | ||||
|      */ | ||||
|     public getStorageTemplateOptions(options?: AxiosRequestConfig) { | ||||
|         return SystemConfigApiFp(this.configuration).getStorageTemplateOptions(options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {SystemConfigDto} systemConfigDto  | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/base.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/base.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.0 | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/common.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/common.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.0 | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/configuration.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/configuration.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.0 | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/index.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/index.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.0 | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
| @@ -59,11 +59,11 @@ input:focus-visible { | ||||
|  | ||||
| @layer utilities { | ||||
| 	.immich-form-input { | ||||
| 		@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-500 dark:disabled:bg-gray-900 disabled:cursor-not-allowed; | ||||
| 		@apply bg-slate-200 p-2 rounded-lg focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-400 dark:disabled:bg-gray-800 disabled:cursor-not-allowed disabled:text-gray-200; | ||||
| 	} | ||||
|  | ||||
| 	.immich-form-label { | ||||
| 		@apply font-medium text-sm text-gray-500 dark:text-gray-300; | ||||
| 		@apply font-medium text-gray-500 dark:text-gray-300; | ||||
| 	} | ||||
|  | ||||
| 	.immich-btn-primary { | ||||
|   | ||||
| @@ -25,12 +25,12 @@ | ||||
| 			const { data: configs } = await api.systemConfigApi.getConfig(); | ||||
|  | ||||
| 			const result = await api.systemConfigApi.updateConfig({ | ||||
| 				ffmpeg: ffmpegConfig, | ||||
| 				oauth: configs.oauth | ||||
| 				...configs, | ||||
| 				ffmpeg: ffmpegConfig | ||||
| 			}); | ||||
|  | ||||
| 			ffmpegConfig = result.data.ffmpeg; | ||||
| 			savedConfig = result.data.ffmpeg; | ||||
| 			ffmpegConfig = { ...result.data.ffmpeg }; | ||||
| 			savedConfig = { ...result.data.ffmpeg }; | ||||
|  | ||||
| 			notificationController.show({ | ||||
| 				message: 'FFmpeg settings saved', | ||||
| @@ -48,8 +48,8 @@ | ||||
| 	async function reset() { | ||||
| 		const { data: resetConfig } = await api.systemConfigApi.getConfig(); | ||||
|  | ||||
| 		ffmpegConfig = resetConfig.ffmpeg; | ||||
| 		savedConfig = resetConfig.ffmpeg; | ||||
| 		ffmpegConfig = { ...resetConfig.ffmpeg }; | ||||
| 		savedConfig = { ...resetConfig.ffmpeg }; | ||||
|  | ||||
| 		notificationController.show({ | ||||
| 			message: 'Reset FFmpeg settings to the recent saved settings', | ||||
| @@ -60,8 +60,8 @@ | ||||
| 	async function resetToDefault() { | ||||
| 		const { data: configs } = await api.systemConfigApi.getDefaults(); | ||||
|  | ||||
| 		ffmpegConfig = configs.ffmpeg; | ||||
| 		defaultConfig = configs.ffmpeg; | ||||
| 		ffmpegConfig = { ...configs.ffmpeg }; | ||||
| 		defaultConfig = { ...configs.ffmpeg }; | ||||
|  | ||||
| 		notificationController.show({ | ||||
| 			message: 'Reset FFmpeg settings to default', | ||||
| @@ -74,52 +74,56 @@ | ||||
| 	{#await getConfigs() then} | ||||
| 		<div in:fade={{ duration: 500 }}> | ||||
| 			<form autocomplete="off" on:submit|preventDefault> | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.NUMBER} | ||||
| 					label="CRF" | ||||
| 					bind:value={ffmpegConfig.crf} | ||||
| 					required={true} | ||||
| 					isEdited={!(ffmpegConfig.crf == savedConfig.crf)} | ||||
| 				/> | ||||
| 				<div class="flex flex-col gap-4 ml-4 mt-4"> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.NUMBER} | ||||
| 						label="CRF" | ||||
| 						bind:value={ffmpegConfig.crf} | ||||
| 						required={true} | ||||
| 						isEdited={!(ffmpegConfig.crf == savedConfig.crf)} | ||||
| 					/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="PRESET" | ||||
| 					bind:value={ffmpegConfig.preset} | ||||
| 					required={true} | ||||
| 					isEdited={!(ffmpegConfig.preset == savedConfig.preset)} | ||||
| 				/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="PRESET" | ||||
| 						bind:value={ffmpegConfig.preset} | ||||
| 						required={true} | ||||
| 						isEdited={!(ffmpegConfig.preset == savedConfig.preset)} | ||||
| 					/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="AUDIO CODEC" | ||||
| 					bind:value={ffmpegConfig.targetAudioCodec} | ||||
| 					required={true} | ||||
| 					isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)} | ||||
| 				/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="AUDIO CODEC" | ||||
| 						bind:value={ffmpegConfig.targetAudioCodec} | ||||
| 						required={true} | ||||
| 						isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)} | ||||
| 					/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="VIDEO CODEC" | ||||
| 					bind:value={ffmpegConfig.targetVideoCodec} | ||||
| 					required={true} | ||||
| 					isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)} | ||||
| 				/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="VIDEO CODEC" | ||||
| 						bind:value={ffmpegConfig.targetVideoCodec} | ||||
| 						required={true} | ||||
| 						isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)} | ||||
| 					/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="SCALING" | ||||
| 					bind:value={ffmpegConfig.targetScaling} | ||||
| 					required={true} | ||||
| 					isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)} | ||||
| 				/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="SCALING" | ||||
| 						bind:value={ffmpegConfig.targetScaling} | ||||
| 						required={true} | ||||
| 						isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)} | ||||
| 					/> | ||||
| 				</div> | ||||
|  | ||||
| 				<SettingButtonsRow | ||||
| 					on:reset={reset} | ||||
| 					on:save={saveSetting} | ||||
| 					on:reset-to-default={resetToDefault} | ||||
| 					showResetToDefault={!_.isEqual(savedConfig, defaultConfig)} | ||||
| 				/> | ||||
| 				<div class="ml-4"> | ||||
| 					<SettingButtonsRow | ||||
| 						on:reset={reset} | ||||
| 						on:save={saveSetting} | ||||
| 						on:reset-to-default={resetToDefault} | ||||
| 						showResetToDefault={!_.isEqual(savedConfig, defaultConfig)} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	{/await} | ||||
|   | ||||
| @@ -25,8 +25,8 @@ | ||||
| 	async function reset() { | ||||
| 		const { data: resetConfig } = await api.systemConfigApi.getConfig(); | ||||
|  | ||||
| 		oauthConfig = resetConfig.oauth; | ||||
| 		savedConfig = resetConfig.oauth; | ||||
| 		oauthConfig = { ...resetConfig.oauth }; | ||||
| 		savedConfig = { ...resetConfig.oauth }; | ||||
|  | ||||
| 		notificationController.show({ | ||||
| 			message: 'Reset OAuth settings to the last saved settings', | ||||
| @@ -39,12 +39,12 @@ | ||||
| 			const { data: currentConfig } = await api.systemConfigApi.getConfig(); | ||||
|  | ||||
| 			const result = await api.systemConfigApi.updateConfig({ | ||||
| 				ffmpeg: currentConfig.ffmpeg, | ||||
| 				...currentConfig, | ||||
| 				oauth: oauthConfig | ||||
| 			}); | ||||
|  | ||||
| 			oauthConfig = result.data.oauth; | ||||
| 			savedConfig = result.data.oauth; | ||||
| 			oauthConfig = { ...result.data.oauth }; | ||||
| 			savedConfig = { ...result.data.oauth }; | ||||
|  | ||||
| 			notificationController.show({ | ||||
| 				message: 'OAuth settings saved', | ||||
| @@ -62,7 +62,7 @@ | ||||
| 	async function resetToDefault() { | ||||
| 		const { data: defaultConfig } = await api.systemConfigApi.getDefaults(); | ||||
|  | ||||
| 		oauthConfig = defaultConfig.oauth; | ||||
| 		oauthConfig = { ...defaultConfig.oauth }; | ||||
|  | ||||
| 		notificationController.show({ | ||||
| 			message: 'Reset OAuth settings to default', | ||||
| @@ -80,51 +80,52 @@ | ||||
| 				</div> | ||||
|  | ||||
| 				<hr class="m-4" /> | ||||
| 				<div class="flex flex-col gap-4 ml-4"> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="ISSUER URL" | ||||
| 						bind:value={oauthConfig.issuerUrl} | ||||
| 						required={true} | ||||
| 						disabled={!oauthConfig.enabled} | ||||
| 						isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)} | ||||
| 					/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="ISSUER URL" | ||||
| 					bind:value={oauthConfig.issuerUrl} | ||||
| 					required={true} | ||||
| 					disabled={!oauthConfig.enabled} | ||||
| 					isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)} | ||||
| 				/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="CLIENT ID" | ||||
| 						bind:value={oauthConfig.clientId} | ||||
| 						required={true} | ||||
| 						disabled={!oauthConfig.enabled} | ||||
| 						isEdited={!(oauthConfig.clientId == savedConfig.clientId)} | ||||
| 					/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="CLIENT ID" | ||||
| 					bind:value={oauthConfig.clientId} | ||||
| 					required={true} | ||||
| 					disabled={!oauthConfig.enabled} | ||||
| 					isEdited={!(oauthConfig.clientId == savedConfig.clientId)} | ||||
| 				/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="CLIENT SECRET" | ||||
| 						bind:value={oauthConfig.clientSecret} | ||||
| 						required={true} | ||||
| 						disabled={!oauthConfig.enabled} | ||||
| 						isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)} | ||||
| 					/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="CLIENT SECRET" | ||||
| 					bind:value={oauthConfig.clientSecret} | ||||
| 					required={true} | ||||
| 					disabled={!oauthConfig.enabled} | ||||
| 					isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)} | ||||
| 				/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="SCOPE" | ||||
| 						bind:value={oauthConfig.scope} | ||||
| 						required={true} | ||||
| 						disabled={!oauthConfig.enabled} | ||||
| 						isEdited={!(oauthConfig.scope == savedConfig.scope)} | ||||
| 					/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="SCOPE" | ||||
| 					bind:value={oauthConfig.scope} | ||||
| 					required={true} | ||||
| 					disabled={!oauthConfig.enabled} | ||||
| 					isEdited={!(oauthConfig.scope == savedConfig.scope)} | ||||
| 				/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					label="BUTTON TEXT" | ||||
| 					bind:value={oauthConfig.buttonText} | ||||
| 					required={false} | ||||
| 					disabled={!oauthConfig.enabled} | ||||
| 					isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)} | ||||
| 				/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="BUTTON TEXT" | ||||
| 						bind:value={oauthConfig.buttonText} | ||||
| 						required={false} | ||||
| 						disabled={!oauthConfig.enabled} | ||||
| 						isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)} | ||||
| 					/> | ||||
| 				</div> | ||||
|  | ||||
| 				<div class="mt-4"> | ||||
| 					<SettingSwitch | ||||
| @@ -135,12 +136,14 @@ | ||||
| 					/> | ||||
| 				</div> | ||||
|  | ||||
| 				<SettingButtonsRow | ||||
| 					on:reset={reset} | ||||
| 					on:save={saveSetting} | ||||
| 					on:reset-to-default={resetToDefault} | ||||
| 					showResetToDefault={!_.isEqual(savedConfig, defaultConfig)} | ||||
| 				/> | ||||
| 				<div class="ml-4"> | ||||
| 					<SettingButtonsRow | ||||
| 						on:reset={reset} | ||||
| 						on:save={saveSetting} | ||||
| 						on:reset-to-default={resetToDefault} | ||||
| 						showResetToDefault={!_.isEqual(savedConfig, defaultConfig)} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	{/await} | ||||
|   | ||||
| @@ -6,11 +6,11 @@ | ||||
| 	export let showResetToDefault = true; | ||||
| </script> | ||||
|  | ||||
| <div class="flex justify-between gap-2 mx-4 mt-8"> | ||||
| <div class="flex justify-between gap-2  mt-8"> | ||||
| 	<div class="left"> | ||||
| 		{#if showResetToDefault} | ||||
| 			<button | ||||
| 				on:click|preventDefault={() => dispatch('reset-to-default')} | ||||
| 				on:click={() => dispatch('reset-to-default')} | ||||
| 				class="text-sm dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75 text-immich-primary hover:text-immich-primary/75 font-medium bg-none" | ||||
| 			> | ||||
| 				Reset to default | ||||
| @@ -20,7 +20,7 @@ | ||||
|  | ||||
| 	<div class="right"> | ||||
| 		<button | ||||
| 			on:click|preventDefault={() => dispatch('reset')} | ||||
| 			on:click={() => dispatch('reset')} | ||||
| 			class="text-sm bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed" | ||||
| 			>Reset | ||||
| 		</button> | ||||
|   | ||||
| @@ -12,19 +12,19 @@ | ||||
|  | ||||
| 	export let inputType: SettingInputFieldType; | ||||
| 	export let value: string; | ||||
| 	export let label: string; | ||||
| 	export let label = ''; | ||||
| 	export let required = false; | ||||
| 	export let disabled = false; | ||||
| 	export let isEdited: boolean; | ||||
| 	export let isEdited = false; | ||||
|  | ||||
| 	const handleInput = (e: Event) => { | ||||
| 		value = (e.target as HTMLInputElement).value; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <div class="m-4 flex flex-col gap-2"> | ||||
| 	<div class="flex place-items-center gap-1"> | ||||
| 		<label class="immich-form-label" for={label}>{label.toUpperCase()} </label> | ||||
| <div class="w-full"> | ||||
| 	<div class={`flex place-items-center gap-1 h-[26px]`}> | ||||
| 		<label class={`immich-form-label text-xs`} for={label}>{label.toUpperCase()} </label> | ||||
| 		{#if required} | ||||
| 			<div class="text-red-400">*</div> | ||||
| 		{/if} | ||||
| @@ -32,14 +32,14 @@ | ||||
| 		{#if isEdited} | ||||
| 			<div | ||||
| 				transition:fly={{ x: 10, duration: 200, easing: quintOut }} | ||||
| 				class="text-gray-500 text-xs italic" | ||||
| 				class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]" | ||||
| 			> | ||||
| 				Unsaved change | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 	<input | ||||
| 		class="immich-form-input" | ||||
| 		class="immich-form-input w-full" | ||||
| 		id={label} | ||||
| 		name={label} | ||||
| 		type={inputType} | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|  | ||||
| <div class="flex justify-between mx-4 place-items-center"> | ||||
| 	<div> | ||||
| 		<h2 class="immich-form-label"> | ||||
| 		<h2 class="immich-form-label text-sm"> | ||||
| 			{title.toUpperCase()} | ||||
| 		</h2> | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,227 @@ | ||||
| <script lang="ts"> | ||||
| 	import { | ||||
| 		api, | ||||
| 		SystemConfigStorageTemplateDto, | ||||
| 		SystemConfigTemplateStorageOptionDto, | ||||
| 		UserResponseDto | ||||
| 	} from '@api'; | ||||
| 	import * as luxon from 'luxon'; | ||||
| 	import handlebar from 'handlebars'; | ||||
| 	import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import SupportedDatetimePanel from './supported-datetime-panel.svelte'; | ||||
| 	import SupportedVariablesPanel from './supported-variables-panel.svelte'; | ||||
| 	import SettingButtonsRow from '../setting-buttons-row.svelte'; | ||||
| 	import _ from 'lodash'; | ||||
| 	import { | ||||
| 		notificationController, | ||||
| 		NotificationType | ||||
| 	} from '$lib/components/shared-components/notification/notification'; | ||||
| 	import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte'; | ||||
|  | ||||
| 	export let storageConfig: SystemConfigStorageTemplateDto; | ||||
| 	export let user: UserResponseDto; | ||||
|  | ||||
| 	let savedConfig: SystemConfigStorageTemplateDto; | ||||
| 	let defaultConfig: SystemConfigStorageTemplateDto; | ||||
| 	let templateOptions: SystemConfigTemplateStorageOptionDto; | ||||
| 	let selectedPreset = ''; | ||||
|  | ||||
| 	async function getConfigs() { | ||||
| 		[savedConfig, defaultConfig, templateOptions] = await Promise.all([ | ||||
| 			api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate), | ||||
| 			api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate), | ||||
| 			api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data) | ||||
| 		]); | ||||
|  | ||||
| 		selectedPreset = templateOptions.presetOptions[0]; | ||||
| 	} | ||||
|  | ||||
| 	const getSupportDateTimeFormat = async () => { | ||||
| 		const { data } = await api.systemConfigApi.getStorageTemplateOptions(); | ||||
| 		return data; | ||||
| 	}; | ||||
|  | ||||
| 	$: parsedTemplate = () => { | ||||
| 		try { | ||||
| 			return renderTemplate(storageConfig.template); | ||||
| 		} catch (error) { | ||||
| 			return 'error'; | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const renderTemplate = (templateString: string) => { | ||||
| 		const template = handlebar.compile(templateString, { | ||||
| 			knownHelpers: undefined | ||||
| 		}); | ||||
|  | ||||
| 		const substitutions: Record<string, string> = { | ||||
| 			filename: 'IMG_10041123', | ||||
| 			ext: 'jpeg' | ||||
| 		}; | ||||
|  | ||||
| 		const dt = luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()); | ||||
|  | ||||
| 		const dateTokens = [ | ||||
| 			...templateOptions.yearOptions, | ||||
| 			...templateOptions.monthOptions, | ||||
| 			...templateOptions.dayOptions, | ||||
| 			...templateOptions.hourOptions, | ||||
| 			...templateOptions.minuteOptions, | ||||
| 			...templateOptions.secondOptions | ||||
| 		]; | ||||
|  | ||||
| 		for (const token of dateTokens) { | ||||
| 			substitutions[token] = dt.toFormat(token); | ||||
| 		} | ||||
|  | ||||
| 		return template(substitutions); | ||||
| 	}; | ||||
|  | ||||
| 	async function reset() { | ||||
| 		const { data: resetConfig } = await api.systemConfigApi.getConfig(); | ||||
|  | ||||
| 		storageConfig.template = resetConfig.storageTemplate.template; | ||||
| 		savedConfig.template = resetConfig.storageTemplate.template; | ||||
|  | ||||
| 		notificationController.show({ | ||||
| 			message: 'Reset storage template settings to the recent saved settings', | ||||
| 			type: NotificationType.Info | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	async function saveSetting() { | ||||
| 		try { | ||||
| 			const { data: currentConfig } = await api.systemConfigApi.getConfig(); | ||||
|  | ||||
| 			const result = await api.systemConfigApi.updateConfig({ | ||||
| 				...currentConfig, | ||||
| 				storageTemplate: storageConfig | ||||
| 			}); | ||||
|  | ||||
| 			storageConfig.template = result.data.storageTemplate.template; | ||||
| 			savedConfig.template = result.data.storageTemplate.template; | ||||
|  | ||||
| 			notificationController.show({ | ||||
| 				message: 'Storage template saved', | ||||
| 				type: NotificationType.Info | ||||
| 			}); | ||||
| 		} catch (e) { | ||||
| 			console.error('Error [storage-template-settings] [saveSetting]', e); | ||||
| 			notificationController.show({ | ||||
| 				message: 'Unable to save settings', | ||||
| 				type: NotificationType.Error | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async function resetToDefault() { | ||||
| 		const { data: defaultConfig } = await api.systemConfigApi.getDefaults(); | ||||
|  | ||||
| 		storageConfig.template = defaultConfig.storageTemplate.template; | ||||
|  | ||||
| 		notificationController.show({ | ||||
| 			message: 'Reset storage template to default', | ||||
| 			type: NotificationType.Info | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	const handlePresetSelection = () => { | ||||
| 		storageConfig.template = selectedPreset; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <section class="dark:text-immich-dark-fg"> | ||||
| 	{#await getConfigs() then} | ||||
| 		<div id="directory-path-builder" class="m-4"> | ||||
| 			<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base"> | ||||
| 				Variables | ||||
| 			</h3> | ||||
|  | ||||
| 			<section class="support-date"> | ||||
| 				{#await getSupportDateTimeFormat()} | ||||
| 					<LoadingSpinner /> | ||||
| 				{:then options} | ||||
| 					<div transition:fade={{ duration: 200 }}> | ||||
| 						<SupportedDatetimePanel {options} /> | ||||
| 					</div> | ||||
| 				{/await} | ||||
| 			</section> | ||||
|  | ||||
| 			<section class="support-date"> | ||||
| 				<SupportedVariablesPanel /> | ||||
| 			</section> | ||||
|  | ||||
| 			<div class="mt-4 flex flex-col"> | ||||
| 				<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base"> | ||||
| 					Template | ||||
| 				</h3> | ||||
|  | ||||
| 				<div class="text-xs my-2"> | ||||
| 					<h4>PREVIEW</h4> | ||||
| 				</div> | ||||
|  | ||||
| 				<p class="text-xs"> | ||||
| 					Approximately path length limit : <span | ||||
| 						class="font-semibold text-immich-primary dark:text-immich-dark-primary" | ||||
| 						>{parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}</span | ||||
| 					>/260 | ||||
| 				</p> | ||||
|  | ||||
| 				<p class="text-xs"> | ||||
| 					{user.id} is the user's ID | ||||
| 				</p> | ||||
|  | ||||
| 				<p | ||||
| 					class="text-xs p-4 bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg py-2 rounded-lg mt-2" | ||||
| 				> | ||||
| 					<span class="text-immich-fg/25 dark:text-immich-dark-fg/50" | ||||
| 						>UPLOAD_LOCATION/{user.id}</span | ||||
| 					>/{parsedTemplate()}.jpeg | ||||
| 				</p> | ||||
|  | ||||
| 				<form autocomplete="off" class="flex flex-col" on:submit|preventDefault> | ||||
| 					<div class="flex flex-col my-2"> | ||||
| 						<label class="text-xs" for="presets">PRESET</label> | ||||
| 						<select | ||||
| 							class="text-sm bg-slate-200 p-2 rounded-lg mt-2 dark:bg-gray-600 hover:cursor-pointer" | ||||
| 							name="presets" | ||||
| 							id="preset-select" | ||||
| 							bind:value={selectedPreset} | ||||
| 							on:change={handlePresetSelection} | ||||
| 						> | ||||
| 							{#each templateOptions.presetOptions as preset} | ||||
| 								<option value={preset}>{renderTemplate(preset)}</option> | ||||
| 							{/each} | ||||
| 						</select> | ||||
| 					</div> | ||||
| 					<div class="flex gap-2 align-bottom"> | ||||
| 						<SettingInputField | ||||
| 							label="template" | ||||
| 							required | ||||
| 							inputType={SettingInputFieldType.TEXT} | ||||
| 							bind:value={storageConfig.template} | ||||
| 							isEdited={!(storageConfig.template === savedConfig.template)} | ||||
| 						/> | ||||
|  | ||||
| 						<div class="flex-0"> | ||||
| 							<SettingInputField | ||||
| 								label="Extension" | ||||
| 								inputType={SettingInputFieldType.TEXT} | ||||
| 								value={'.jpeg'} | ||||
| 								disabled | ||||
| 							/> | ||||
| 						</div> | ||||
| 					</div> | ||||
|  | ||||
| 					<SettingButtonsRow | ||||
| 						on:reset={reset} | ||||
| 						on:save={saveSetting} | ||||
| 						on:reset-to-default={resetToDefault} | ||||
| 						showResetToDefault={!_.isEqual(savedConfig, defaultConfig)} | ||||
| 					/> | ||||
| 				</form> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{/await} | ||||
| </section> | ||||
| @@ -0,0 +1,78 @@ | ||||
| <script lang="ts"> | ||||
| 	import { SystemConfigTemplateStorageOptionDto } from '@api'; | ||||
| 	import * as luxon from 'luxon'; | ||||
|  | ||||
| 	export let options: SystemConfigTemplateStorageOptionDto; | ||||
|  | ||||
| 	const getLuxonExample = (format: string) => { | ||||
| 		return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat( | ||||
| 			format | ||||
| 		); | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <div class="text-xs mt-2"> | ||||
| 	<h4>DATE & TIME</h4> | ||||
| </div> | ||||
|  | ||||
| <div class="text-xs bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg p-4 mt-2 rounded-lg"> | ||||
| 	<div class="mb-2 text-gray-600 dark:text-immich-dark-fg"> | ||||
| 		<p>Asset's creation timestamp is used for the datetime information</p> | ||||
| 		<p>Sample time 2022-09-04T20:03:05.250</p> | ||||
| 	</div> | ||||
| 	<div class="flex gap-[50px]"> | ||||
| 		<div> | ||||
| 			<p class="text-immich-primary font-medium dark:text-immich-dark-primary">YEAR</p> | ||||
| 			<ul> | ||||
| 				{#each options.yearOptions as yearFormat} | ||||
| 					<li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li> | ||||
| 				{/each} | ||||
| 			</ul> | ||||
| 		</div> | ||||
|  | ||||
| 		<div> | ||||
| 			<p class="text-immich-primary font-medium dark:text-immich-dark-primary">MONTH</p> | ||||
| 			<ul> | ||||
| 				{#each options.monthOptions as monthFormat} | ||||
| 					<li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li> | ||||
| 				{/each} | ||||
| 			</ul> | ||||
| 		</div> | ||||
|  | ||||
| 		<div> | ||||
| 			<p class="text-immich-primary font-medium dark:text-immich-dark-primary">DAY</p> | ||||
| 			<ul> | ||||
| 				{#each options.dayOptions as dayFormat} | ||||
| 					<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li> | ||||
| 				{/each} | ||||
| 			</ul> | ||||
| 		</div> | ||||
|  | ||||
| 		<div> | ||||
| 			<p class="text-immich-primary font-medium dark:text-immich-dark-primary">HOUR</p> | ||||
| 			<ul> | ||||
| 				{#each options.hourOptions as dayFormat} | ||||
| 					<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li> | ||||
| 				{/each} | ||||
| 			</ul> | ||||
| 		</div> | ||||
|  | ||||
| 		<div> | ||||
| 			<p class="text-immich-primary font-medium dark:text-immich-dark-primary">MINUTE</p> | ||||
| 			<ul> | ||||
| 				{#each options.minuteOptions as dayFormat} | ||||
| 					<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li> | ||||
| 				{/each} | ||||
| 			</ul> | ||||
| 		</div> | ||||
|  | ||||
| 		<div> | ||||
| 			<p class="text-immich-primary font-medium dark:text-immich-dark-primary">SECOND</p> | ||||
| 			<ul> | ||||
| 				{#each options.secondOptions as dayFormat} | ||||
| 					<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li> | ||||
| 				{/each} | ||||
| 			</ul> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -0,0 +1,21 @@ | ||||
| <div class="text-xs mt-4"> | ||||
| 	<h4>OTHER VARIABLES</h4> | ||||
| </div> | ||||
|  | ||||
| <div class="text-xs bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg p-4 mt-2 rounded-lg"> | ||||
| 	<div class="flex gap-[50px]"> | ||||
| 		<div> | ||||
| 			<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE NAME</p> | ||||
| 			<ul> | ||||
| 				<li>{`{{filename}}`}</li> | ||||
| 			</ul> | ||||
| 		</div> | ||||
|  | ||||
| 		<div> | ||||
| 			<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE EXTENSION</p> | ||||
| 			<ul> | ||||
| 				<li>{`{{ext}}`}</li> | ||||
| 			</ul> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -2,11 +2,13 @@ | ||||
| 	import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; | ||||
| 	import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte'; | ||||
| 	import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte'; | ||||
| 	import StorageTemplateSettings from '$lib/components/admin-page/settings/storate-template/storage-template-settings.svelte'; | ||||
| 	import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | ||||
| 	import { api, SystemConfigDto } from '@api'; | ||||
| 	import type { PageData } from './$types'; | ||||
|  | ||||
| 	let systemConfig: SystemConfigDto; | ||||
|  | ||||
| 	export let data: PageData; | ||||
| 	const getConfig = async () => { | ||||
| 		const { data } = await api.systemConfigApi.getConfig(); | ||||
| 		systemConfig = data; | ||||
| @@ -33,5 +35,12 @@ | ||||
| 		<SettingAccordion title="OAuth Settings" subtitle="Manage the OAuth integration to Immich app"> | ||||
| 			<OAuthSettings oauthConfig={configs.oauth} /> | ||||
| 		</SettingAccordion> | ||||
|  | ||||
| 		<SettingAccordion | ||||
| 			title="Storage Template" | ||||
| 			subtitle="Manage the folder structure and file name of the upload asset" | ||||
| 		> | ||||
| 			<StorageTemplateSettings storageConfig={configs.storageTemplate} user={data.user} /> | ||||
| 		</SettingAccordion> | ||||
| 	{/await} | ||||
| </section> | ||||
|   | ||||
| @@ -22,7 +22,6 @@ | ||||
|  | ||||
| 	onMount(() => { | ||||
| 		allUsers = $page.data.allUsers; | ||||
| 		console.log('getting all users', allUsers); | ||||
| 	}); | ||||
|  | ||||
| 	const isDeleted = (user: UserResponseDto): boolean => { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user