diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 608df54b10971..a596cc0d83fd6 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 4b4d076f649d7..e29aec936e160 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -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) diff --git a/mobile/openapi/doc/SystemConfigApi.md b/mobile/openapi/doc/SystemConfigApi.md index 365ac3f7642fd..ae8d234ccf94f 100644 --- a/mobile/openapi/doc/SystemConfigApi.md +++ b/mobile/openapi/doc/SystemConfigApi.md @@ -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('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('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) diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index af283c4fdeb89..19209682fe5dd 100644 --- a/mobile/openapi/doc/SystemConfigDto.md +++ b/mobile/openapi/doc/SystemConfigDto.md @@ -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) diff --git a/mobile/openapi/doc/SystemConfigStorageTemplateDto.md b/mobile/openapi/doc/SystemConfigStorageTemplateDto.md new file mode 100644 index 0000000000000..88bfe4569bdf6 --- /dev/null +++ b/mobile/openapi/doc/SystemConfigStorageTemplateDto.md @@ -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) + + diff --git a/mobile/openapi/doc/SystemConfigTemplateStorageOptionDto.md b/mobile/openapi/doc/SystemConfigTemplateStorageOptionDto.md new file mode 100644 index 0000000000000..5aa9aa195ea31 --- /dev/null +++ b/mobile/openapi/doc/SystemConfigTemplateStorageOptionDto.md @@ -0,0 +1,21 @@ +# openapi.model.SystemConfigTemplateStorageOptionDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**yearOptions** | **List** | | [default to const []] +**monthOptions** | **List** | | [default to const []] +**dayOptions** | **List** | | [default to const []] +**hourOptions** | **List** | | [default to const []] +**minuteOptions** | **List** | | [default to const []] +**secondOptions** | **List** | | [default to const []] +**presetOptions** | **List** | | [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) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index b30e4f6814864..e227bb7529162 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -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'; diff --git a/mobile/openapi/lib/api/system_config_api.dart b/mobile/openapi/lib/api/system_config_api.dart index 7bd66c670078b..d2d0ac5ba50fb 100644 --- a/mobile/openapi/lib/api/system_config_api.dart +++ b/mobile/openapi/lib/api/system_config_api.dart @@ -98,6 +98,47 @@ class SystemConfigApi { return null; } + /// Performs an HTTP 'GET /system-config/storage-template-options' operation and returns the [Response]. + Future getStorageTemplateOptionsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/system-config/storage-template-options'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future 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: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 628b533e7e2a4..0fd1705933797 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -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': diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index a667236e748f7..22701819a931a 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -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 toJson() { final _json = {}; _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 = { 'ffmpeg', 'oauth', + 'storageTemplate', }; } diff --git a/mobile/openapi/lib/model/system_config_storage_template_dto.dart b/mobile/openapi/lib/model/system_config_storage_template_dto.dart new file mode 100644 index 0000000000000..482641484f7b7 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_storage_template_dto.dart @@ -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 toJson() { + final _json = {}; + _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(); + + // 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(json, r'template')!, + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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 mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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 = { + 'template', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart new file mode 100644 index 0000000000000..a0a799bd09d16 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart @@ -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 yearOptions; + + List monthOptions; + + List dayOptions; + + List hourOptions; + + List minuteOptions; + + List secondOptions; + + List 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 toJson() { + final _json = {}; + _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(); + + // 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() + : const [], + monthOptions: json[r'monthOptions'] is List + ? (json[r'monthOptions'] as List).cast() + : const [], + dayOptions: json[r'dayOptions'] is List + ? (json[r'dayOptions'] as List).cast() + : const [], + hourOptions: json[r'hourOptions'] is List + ? (json[r'hourOptions'] as List).cast() + : const [], + minuteOptions: json[r'minuteOptions'] is List + ? (json[r'minuteOptions'] as List).cast() + : const [], + secondOptions: json[r'secondOptions'] is List + ? (json[r'secondOptions'] as List).cast() + : const [], + presetOptions: json[r'presetOptions'] is List + ? (json[r'presetOptions'] as List).cast() + : const [], + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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 mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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 = { + 'yearOptions', + 'monthOptions', + 'dayOptions', + 'hourOptions', + 'minuteOptions', + 'secondOptions', + 'presetOptions', + }; +} + diff --git a/mobile/openapi/test/system_config_api_test.dart b/mobile/openapi/test/system_config_api_test.dart index 84d1ceeb909cf..6cb7aa79d1d26 100644 --- a/mobile/openapi/test/system_config_api_test.dart +++ b/mobile/openapi/test/system_config_api_test.dart @@ -27,6 +27,11 @@ void main() { // TODO }); + //Future getStorageTemplateOptions() async + test('test getStorageTemplateOptions', () async { + // TODO + }); + //Future updateConfig(SystemConfigDto systemConfigDto) async test('test updateConfig', () async { // TODO diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index 1cf6c7f23ce98..b4a3172ce375f 100644 --- a/mobile/openapi/test/system_config_dto_test.dart +++ b/mobile/openapi/test/system_config_dto_test.dart @@ -26,6 +26,11 @@ void main() { // TODO }); + // SystemConfigStorageTemplateDto storageTemplate + test('to test the property `storageTemplate`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/system_config_storage_template_dto_test.dart b/mobile/openapi/test/system_config_storage_template_dto_test.dart new file mode 100644 index 0000000000000..9f5a2e5e10af4 --- /dev/null +++ b/mobile/openapi/test/system_config_storage_template_dto_test.dart @@ -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 + }); + + + }); + +} diff --git a/mobile/openapi/test/system_config_template_storage_option_dto_test.dart b/mobile/openapi/test/system_config_template_storage_option_dto_test.dart new file mode 100644 index 0000000000000..2824cc64d2667 --- /dev/null +++ b/mobile/openapi/test/system_config_template_storage_option_dto_test.dart @@ -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 yearOptions (default value: const []) + test('to test the property `yearOptions`', () async { + // TODO + }); + + // List monthOptions (default value: const []) + test('to test the property `monthOptions`', () async { + // TODO + }); + + // List dayOptions (default value: const []) + test('to test the property `dayOptions`', () async { + // TODO + }); + + // List hourOptions (default value: const []) + test('to test the property `hourOptions`', () async { + // TODO + }); + + // List minuteOptions (default value: const []) + test('to test the property `minuteOptions`', () async { + // TODO + }); + + // List secondOptions (default value: const []) + test('to test the property `secondOptions`', () async { + // TODO + }); + + // List presetOptions (default value: const []) + test('to test the property `presetOptions`', () async { + // TODO + }); + + + }); + +} diff --git a/notes.md b/notes.md new file mode 100644 index 0000000000000..74e97d5d980a3 --- /dev/null +++ b/notes.md @@ -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 \ No newline at end of file diff --git a/server/apps/immich/src/api-v1/asset/asset.module.ts b/server/apps/immich/src/api-v1/asset/asset.module.ts index c8501426e29b4..bdd489223df10 100644 --- a/server/apps/immich/src/api-v1/asset/asset.module.ts +++ b/server/apps/immich/src/api-v1/asset/asset.module.ts @@ -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, diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index 7f19e0684e10f..fed76c96465a9 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -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; let assetUploadedQueueMock: jest.Mocked>; let videoConversionQueueMock: jest.Mocked>; + let storageSeriveMock: jest.Mocked; 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, ); }); diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 577e08525c5d4..b0f9b39f36513 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -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, 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([ { diff --git a/server/apps/immich/src/api-v1/system-config/dto/system-config-storage-template.dto.ts b/server/apps/immich/src/api-v1/system-config/dto/system-config-storage-template.dto.ts new file mode 100644 index 0000000000000..f00006583c3a4 --- /dev/null +++ b/server/apps/immich/src/api-v1/system-config/dto/system-config-storage-template.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class SystemConfigStorageTemplateDto { + @IsNotEmpty() + @IsString() + template!: string; +} diff --git a/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts b/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts index 1cbb5e3666cf7..72e1356ba4889 100644 --- a/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts +++ b/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts @@ -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 { diff --git a/server/apps/immich/src/api-v1/system-config/response-dto/system-config-template-storage-option.dto.ts b/server/apps/immich/src/api-v1/system-config/response-dto/system-config-template-storage-option.dto.ts new file mode 100644 index 0000000000000..c9150f1ddd9eb --- /dev/null +++ b/server/apps/immich/src/api-v1/system-config/response-dto/system-config-template-storage-option.dto.ts @@ -0,0 +1,9 @@ +export class SystemConfigTemplateStorageOptionDto { + yearOptions!: string[]; + monthOptions!: string[]; + dayOptions!: string[]; + hourOptions!: string[]; + minuteOptions!: string[]; + secondOptions!: string[]; + presetOptions!: string[]; +} diff --git a/server/apps/immich/src/api-v1/system-config/system-config.controller.ts b/server/apps/immich/src/api-v1/system-config/system-config.controller.ts index 4b8cb297992a7..ed247b382e753 100644 --- a/server/apps/immich/src/api-v1/system-config/system-config.controller.ts +++ b/server/apps/immich/src/api-v1/system-config/system-config.controller.ts @@ -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 { return this.systemConfigService.updateConfig(dto); } + + @Get('storage-template-options') + public getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { + return this.systemConfigService.getStorageTemplateOptions(); + } } diff --git a/server/apps/immich/src/api-v1/system-config/system-config.service.ts b/server/apps/immich/src/api-v1/system-config/system-config.service.ts index 5426d5a3109f3..db02cb5f20531 100644 --- a/server/apps/immich/src/api-v1/system-config/system-config.service.ts +++ b/server/apps/immich/src/api-v1/system-config/system-config.service.ts @@ -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 { - 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; } } diff --git a/server/apps/immich/src/api-v1/user/user.service.spec.ts b/server/apps/immich/src/api-v1/user/user.service.spec.ts index 126d9256075a5..4ac08c4f9471d 100644 --- a/server/apps/immich/src/api-v1/user/user.service.spec.ts +++ b/server/apps/immich/src/api-v1/user/user.service.spec.ts @@ -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', () => { diff --git a/server/apps/microservices/src/processors/thumbnail.processor.ts b/server/apps/microservices/src/processors/thumbnail.processor.ts index 6cf684c5cd937..c01e3da2aacfd 100644 --- a/server/apps/microservices/src/processors/thumbnail.processor.ts +++ b/server/apps/microservices/src/processors/thumbnail.processor.ts @@ -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 { diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 8adc1924d5eaa..f306868cf6049 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -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" ] } } diff --git a/server/libs/database/src/entities/system-config.entity.ts b/server/libs/database/src/entities/system-config.entity.ts index ce3fd7a96edf2..646a89f9f49d3 100644 --- a/server/libs/database/src/entities/system-config.entity.ts +++ b/server/libs/database/src/entities/system-config.entity.ts @@ -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; + }; } diff --git a/server/libs/immich-config/src/immich-config.module.ts b/server/libs/immich-config/src/immich-config.module.ts index 14e6897830f0c..dc2b93569f0d4 100644 --- a/server/libs/immich-config/src/immich-config.module.ts +++ b/server/libs/immich-config/src/immich-config.module.ts @@ -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 {} diff --git a/server/libs/immich-config/src/immich-config.service.ts b/server/libs/immich-config/src/immich-config.service.ts index e2656f6fe9caa..157b2d7a62a94 100644 --- a/server/libs/immich-config/src/immich-config.service.ts +++ b/server/libs/immich-config/src/immich-config.service.ts @@ -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; + 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(); + constructor( @InjectRepository(SystemConfigEntity) private systemConfigRepository: Repository, @@ -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 = {}; @@ -45,7 +61,16 @@ export class ImmichConfigService { return _.defaultsDeep(config, defaults) as SystemConfig; } - public async updateConfig(config: DeepPartial | null): Promise { + public async updateConfig(config: SystemConfig): Promise { + 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; } } diff --git a/server/libs/storage/src/constants/supported-datetime-template.ts b/server/libs/storage/src/constants/supported-datetime-template.ts new file mode 100644 index 0000000000000..e6d9bc723d5c5 --- /dev/null +++ b/server/libs/storage/src/constants/supported-datetime-template.ts @@ -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}}', +]; diff --git a/server/libs/storage/src/index.ts b/server/libs/storage/src/index.ts new file mode 100644 index 0000000000000..5c65021d000b6 --- /dev/null +++ b/server/libs/storage/src/index.ts @@ -0,0 +1,2 @@ +export * from './storage.module'; +export * from './storage.service'; diff --git a/server/libs/storage/src/interfaces/immich-storage.interface.ts b/server/libs/storage/src/interfaces/immich-storage.interface.ts new file mode 100644 index 0000000000000..c5d848e1d68a1 --- /dev/null +++ b/server/libs/storage/src/interfaces/immich-storage.interface.ts @@ -0,0 +1,6 @@ +export interface IImmichStorage { + write(): Promise; + read(): Promise; +} + +export enum IStorageType {} diff --git a/server/libs/storage/src/storage.module.ts b/server/libs/storage/src/storage.module.ts new file mode 100644 index 0000000000000..2b29959a2e0f4 --- /dev/null +++ b/server/libs/storage/src/storage.module.ts @@ -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 {} diff --git a/server/libs/storage/src/storage.service.ts b/server/libs/storage/src/storage.service.ts new file mode 100644 index 0000000000000..e714a10390263 --- /dev/null +++ b/server/libs/storage/src/storage.service.ts @@ -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(mv); + +@Injectable() +export class StorageService { + readonly log = new Logger(StorageService.name); + + private storageTemplate: HandlebarsTemplateDelegate; + + constructor( + @InjectRepository(AssetEntity) + private assetRepository: Repository, + 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 { + 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 { + return moveFile(source, destination, { mkdirp: true, clobber: false }); + } + + private async checkFileExist(path: string): Promise { + 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, asset: AssetEntity, filename: string, ext: string) { + const substitutions: Record = { + 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); + } +} diff --git a/server/libs/storage/tsconfig.lib.json b/server/libs/storage/tsconfig.lib.json new file mode 100644 index 0000000000000..b99dca80105d6 --- /dev/null +++ b/server/libs/storage/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/storage" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/server/nest-cli.json b/server/nest-cli.json index 861e733dabe4e..03040b77ca838 100644 --- a/server/nest-cli.json +++ b/server/nest-cli.json @@ -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" + } } } -} +} \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 43af398572ee9..c920dd314a9a6 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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", diff --git a/server/package.json b/server/package.json index 834484af840f6..8e781aeadac3c 100644 --- a/server/package.json +++ b/server/package.json @@ -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": "/libs/database/src/config", "@app/common": "/libs/common/src", "^@app/job(|/.*)$": "/libs/job/src/$1", - "^@app/immich-config(|/.*)$": "/libs/immich-config/src/$1" + "^@app/immich-config(|/.*)$": "/libs/immich-config/src/$1", + "^@app/storage(|/.*)$": "/libs/storage/src/$1" } } } diff --git a/server/tsconfig.json b/server/tsconfig.json index ee830c64a4bcd..530c06b340866 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index cdce9994ea9e7..1b28b5b08a9cc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index 2aea1179fffe6..ac3e092d7b73c 100644 --- a/web/package.json +++ b/web/package.json @@ -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" diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 1fe7093729f0a..7392b8fb14a16 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -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} + * @memberof SystemConfigTemplateStorageOptionDto + */ + 'yearOptions': Array; + /** + * + * @type {Array} + * @memberof SystemConfigTemplateStorageOptionDto + */ + 'monthOptions': Array; + /** + * + * @type {Array} + * @memberof SystemConfigTemplateStorageOptionDto + */ + 'dayOptions': Array; + /** + * + * @type {Array} + * @memberof SystemConfigTemplateStorageOptionDto + */ + 'hourOptions': Array; + /** + * + * @type {Array} + * @memberof SystemConfigTemplateStorageOptionDto + */ + 'minuteOptions': Array; + /** + * + * @type {Array} + * @memberof SystemConfigTemplateStorageOptionDto + */ + 'secondOptions': Array; + /** + * + * @type {Array} + * @memberof SystemConfigTemplateStorageOptionDto + */ + 'presetOptions': Array; +} /** * * @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 => { + 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> { + 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 { return localVarFp.getDefaults(options).then((request) => request(axios, basePath)); }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getStorageTemplateOptions(options?: any): AxiosPromise { + 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 diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index 80827a1d03e7e..f00f196d8b492 100644 --- a/web/src/api/open-api/base.ts +++ b/web/src/api/open-api/base.ts @@ -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). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index 7eead1834cd46..a946fabb549ef 100644 --- a/web/src/api/open-api/common.ts +++ b/web/src/api/open-api/common.ts @@ -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). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index d582ee063904e..48794159a394a 100644 --- a/web/src/api/open-api/configuration.ts +++ b/web/src/api/open-api/configuration.ts @@ -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). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index dc9b61d1910e6..f0b9d9c785a33 100644 --- a/web/src/api/open-api/index.ts +++ b/web/src/api/open-api/index.ts @@ -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). diff --git a/web/src/app.css b/web/src/app.css index 766d4d19f3fd6..a565023d3f094 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -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 { diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index 4359784d01350..a3d9d6fddbea0 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -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}
- +
+ - + - + - + - + +
- +
+ +
{/await} diff --git a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte index e35e9b1e95940..f444a0a9638b2 100644 --- a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte @@ -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 @@
+
+ - + - + - + - - - + +
- +
+ +
{/await} diff --git a/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte b/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte index 0bdd9dc834b80..3605fc4067ff9 100644 --- a/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte +++ b/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte @@ -6,11 +6,11 @@ export let showResetToDefault = true; -
+
{#if showResetToDefault} diff --git a/web/src/lib/components/admin-page/settings/setting-input-field.svelte b/web/src/lib/components/admin-page/settings/setting-input-field.svelte index f45ff08f9b09b..5319d7cf5c30c 100644 --- a/web/src/lib/components/admin-page/settings/setting-input-field.svelte +++ b/web/src/lib/components/admin-page/settings/setting-input-field.svelte @@ -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; }; -
-
- +
+
+ {#if required}
*
{/if} @@ -32,14 +32,14 @@ {#if isEdited}
Unsaved change
{/if}
-

+

{title.toUpperCase()}

diff --git a/web/src/lib/components/admin-page/settings/storate-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storate-template/storage-template-settings.svelte new file mode 100644 index 0000000000000..b21924a876c4e --- /dev/null +++ b/web/src/lib/components/admin-page/settings/storate-template/storage-template-settings.svelte @@ -0,0 +1,227 @@ + + +
+ {#await getConfigs() then} +
+

+ Variables +

+ +
+ {#await getSupportDateTimeFormat()} + + {:then options} +
+ +
+ {/await} +
+ +
+ +
+ +
+

+ Template +

+ +
+

PREVIEW

+
+ +

+ Approximately path length limit : {parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}/260 +

+ +

+ {user.id} is the user's ID +

+ +

+ UPLOAD_LOCATION/{user.id}/{parsedTemplate()}.jpeg +

+ +
+
+ + +
+
+ + +
+ +
+
+ + + +
+
+ {/await} +
diff --git a/web/src/lib/components/admin-page/settings/storate-template/supported-datetime-panel.svelte b/web/src/lib/components/admin-page/settings/storate-template/supported-datetime-panel.svelte new file mode 100644 index 0000000000000..14c817b1ed6b0 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/storate-template/supported-datetime-panel.svelte @@ -0,0 +1,78 @@ + + +
+

DATE & TIME

+
+ +
+
+

Asset's creation timestamp is used for the datetime information

+

Sample time 2022-09-04T20:03:05.250

+
+
+
+

YEAR

+
    + {#each options.yearOptions as yearFormat} +
  • {'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}
  • + {/each} +
+
+ +
+

MONTH

+
    + {#each options.monthOptions as monthFormat} +
  • {'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}
  • + {/each} +
+
+ +
+

DAY

+
    + {#each options.dayOptions as dayFormat} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
+ +
+

HOUR

+
    + {#each options.hourOptions as dayFormat} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
+ +
+

MINUTE

+
    + {#each options.minuteOptions as dayFormat} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
+ +
+

SECOND

+
    + {#each options.secondOptions as dayFormat} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
+
+
diff --git a/web/src/lib/components/admin-page/settings/storate-template/supported-variables-panel.svelte b/web/src/lib/components/admin-page/settings/storate-template/supported-variables-panel.svelte new file mode 100644 index 0000000000000..1eb252e0ac15c --- /dev/null +++ b/web/src/lib/components/admin-page/settings/storate-template/supported-variables-panel.svelte @@ -0,0 +1,21 @@ +
+

OTHER VARIABLES

+
+ +
+
+
+

FILE NAME

+
    +
  • {`{{filename}}`}
  • +
+
+ +
+

FILE EXTENSION

+
    +
  • {`{{ext}}`}
  • +
+
+
+
diff --git a/web/src/routes/admin/settings/+page.svelte b/web/src/routes/admin/settings/+page.svelte index c236ab4c24e0e..4698e84c87069 100644 --- a/web/src/routes/admin/settings/+page.svelte +++ b/web/src/routes/admin/settings/+page.svelte @@ -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 @@ + + + + {/await} diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index a5cb6de6ea6d9..6efc6c696647d 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -22,7 +22,6 @@ onMount(() => { allUsers = $page.data.allUsers; - console.log('getting all users', allUsers); }); const isDeleted = (user: UserResponseDto): boolean => {