mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 20:07:41 +09:00 
			
		
		
		
	feat: nightly tasks (#19879)
This commit is contained in:
		| @@ -15,12 +15,6 @@ describe('/system-config', () => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('PUT /system-config', () => { |   describe('PUT /system-config', () => { | ||||||
|     it('should require authentication', async () => { |  | ||||||
|       const { status, body } = await request(app).put('/system-config'); |  | ||||||
|       expect(status).toBe(401); |  | ||||||
|       expect(body).toEqual(errorDto.unauthorized); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should always return the new config', async () => { |     it('should always return the new config', async () => { | ||||||
|       const config = await getSystemConfig(admin.accessToken); |       const config = await getSystemConfig(admin.accessToken); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								i18n/en.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								i18n/en.json
									
									
									
									
									
								
							| @@ -166,6 +166,20 @@ | |||||||
|     "metadata_settings_description": "Manage metadata settings", |     "metadata_settings_description": "Manage metadata settings", | ||||||
|     "migration_job": "Migration", |     "migration_job": "Migration", | ||||||
|     "migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure", |     "migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure", | ||||||
|  |     "nightly_tasks_cluster_faces_setting_description": "Run facial recognition on newly detected faces", | ||||||
|  |     "nightly_tasks_cluster_new_faces_setting": "Cluster new faces", | ||||||
|  |     "nightly_tasks_database_cleanup_setting": "Database cleanup tasks", | ||||||
|  |     "nightly_tasks_database_cleanup_setting_description": "Clean up old, expired data from the database", | ||||||
|  |     "nightly_tasks_generate_memories_setting": "Generate memories", | ||||||
|  |     "nightly_tasks_generate_memories_setting_description": "Create new memories from assets", | ||||||
|  |     "nightly_tasks_missing_thumbnails_setting": "Generate missing thumbnails", | ||||||
|  |     "nightly_tasks_missing_thumbnails_setting_description": "Queue assets without thumbnails for thumbnail generation", | ||||||
|  |     "nightly_tasks_settings": "Nightly Tasks Settings", | ||||||
|  |     "nightly_tasks_settings_description": "Manage nightly tasks", | ||||||
|  |     "nightly_tasks_start_time_setting": "Start time", | ||||||
|  |     "nightly_tasks_start_time_setting_description": "The time at which the server starts running the nightly tasks", | ||||||
|  |     "nightly_tasks_sync_quota_usage_setting": "Sync quota usage", | ||||||
|  |     "nightly_tasks_sync_quota_usage_setting_description": "Update user storage quota, based on current usage", | ||||||
|     "no_paths_added": "No paths added", |     "no_paths_added": "No paths added", | ||||||
|     "no_pattern_added": "No pattern added", |     "no_pattern_added": "No pattern added", | ||||||
|     "note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the", |     "note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the", | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -509,6 +509,7 @@ Class | Method | HTTP request | Description | |||||||
|  - [SystemConfigMapDto](doc//SystemConfigMapDto.md) |  - [SystemConfigMapDto](doc//SystemConfigMapDto.md) | ||||||
|  - [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md) |  - [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md) | ||||||
|  - [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md) |  - [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md) | ||||||
|  |  - [SystemConfigNightlyTasksDto](doc//SystemConfigNightlyTasksDto.md) | ||||||
|  - [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md) |  - [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md) | ||||||
|  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) |  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) | ||||||
|  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) |  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -291,6 +291,7 @@ part 'model/system_config_machine_learning_dto.dart'; | |||||||
| part 'model/system_config_map_dto.dart'; | part 'model/system_config_map_dto.dart'; | ||||||
| part 'model/system_config_metadata_dto.dart'; | part 'model/system_config_metadata_dto.dart'; | ||||||
| part 'model/system_config_new_version_check_dto.dart'; | part 'model/system_config_new_version_check_dto.dart'; | ||||||
|  | part 'model/system_config_nightly_tasks_dto.dart'; | ||||||
| part 'model/system_config_notifications_dto.dart'; | part 'model/system_config_notifications_dto.dart'; | ||||||
| part 'model/system_config_o_auth_dto.dart'; | part 'model/system_config_o_auth_dto.dart'; | ||||||
| part 'model/system_config_password_login_dto.dart'; | part 'model/system_config_password_login_dto.dart'; | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -638,6 +638,8 @@ class ApiClient { | |||||||
|           return SystemConfigMetadataDto.fromJson(value); |           return SystemConfigMetadataDto.fromJson(value); | ||||||
|         case 'SystemConfigNewVersionCheckDto': |         case 'SystemConfigNewVersionCheckDto': | ||||||
|           return SystemConfigNewVersionCheckDto.fromJson(value); |           return SystemConfigNewVersionCheckDto.fromJson(value); | ||||||
|  |         case 'SystemConfigNightlyTasksDto': | ||||||
|  |           return SystemConfigNightlyTasksDto.fromJson(value); | ||||||
|         case 'SystemConfigNotificationsDto': |         case 'SystemConfigNotificationsDto': | ||||||
|           return SystemConfigNotificationsDto.fromJson(value); |           return SystemConfigNotificationsDto.fromJson(value); | ||||||
|         case 'SystemConfigOAuthDto': |         case 'SystemConfigOAuthDto': | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -23,6 +23,7 @@ class SystemConfigDto { | |||||||
|     required this.map, |     required this.map, | ||||||
|     required this.metadata, |     required this.metadata, | ||||||
|     required this.newVersionCheck, |     required this.newVersionCheck, | ||||||
|  |     required this.nightlyTasks, | ||||||
|     required this.notifications, |     required this.notifications, | ||||||
|     required this.oauth, |     required this.oauth, | ||||||
|     required this.passwordLogin, |     required this.passwordLogin, | ||||||
| @@ -55,6 +56,8 @@ class SystemConfigDto { | |||||||
| 
 | 
 | ||||||
|   SystemConfigNewVersionCheckDto newVersionCheck; |   SystemConfigNewVersionCheckDto newVersionCheck; | ||||||
| 
 | 
 | ||||||
|  |   SystemConfigNightlyTasksDto nightlyTasks; | ||||||
|  | 
 | ||||||
|   SystemConfigNotificationsDto notifications; |   SystemConfigNotificationsDto notifications; | ||||||
| 
 | 
 | ||||||
|   SystemConfigOAuthDto oauth; |   SystemConfigOAuthDto oauth; | ||||||
| @@ -87,6 +90,7 @@ class SystemConfigDto { | |||||||
|     other.map == map && |     other.map == map && | ||||||
|     other.metadata == metadata && |     other.metadata == metadata && | ||||||
|     other.newVersionCheck == newVersionCheck && |     other.newVersionCheck == newVersionCheck && | ||||||
|  |     other.nightlyTasks == nightlyTasks && | ||||||
|     other.notifications == notifications && |     other.notifications == notifications && | ||||||
|     other.oauth == oauth && |     other.oauth == oauth && | ||||||
|     other.passwordLogin == passwordLogin && |     other.passwordLogin == passwordLogin && | ||||||
| @@ -111,6 +115,7 @@ class SystemConfigDto { | |||||||
|     (map.hashCode) + |     (map.hashCode) + | ||||||
|     (metadata.hashCode) + |     (metadata.hashCode) + | ||||||
|     (newVersionCheck.hashCode) + |     (newVersionCheck.hashCode) + | ||||||
|  |     (nightlyTasks.hashCode) + | ||||||
|     (notifications.hashCode) + |     (notifications.hashCode) + | ||||||
|     (oauth.hashCode) + |     (oauth.hashCode) + | ||||||
|     (passwordLogin.hashCode) + |     (passwordLogin.hashCode) + | ||||||
| @@ -123,7 +128,7 @@ class SystemConfigDto { | |||||||
|     (user.hashCode); |     (user.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]'; |   String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, nightlyTasks=$nightlyTasks, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @@ -137,6 +142,7 @@ class SystemConfigDto { | |||||||
|       json[r'map'] = this.map; |       json[r'map'] = this.map; | ||||||
|       json[r'metadata'] = this.metadata; |       json[r'metadata'] = this.metadata; | ||||||
|       json[r'newVersionCheck'] = this.newVersionCheck; |       json[r'newVersionCheck'] = this.newVersionCheck; | ||||||
|  |       json[r'nightlyTasks'] = this.nightlyTasks; | ||||||
|       json[r'notifications'] = this.notifications; |       json[r'notifications'] = this.notifications; | ||||||
|       json[r'oauth'] = this.oauth; |       json[r'oauth'] = this.oauth; | ||||||
|       json[r'passwordLogin'] = this.passwordLogin; |       json[r'passwordLogin'] = this.passwordLogin; | ||||||
| @@ -169,6 +175,7 @@ class SystemConfigDto { | |||||||
|         map: SystemConfigMapDto.fromJson(json[r'map'])!, |         map: SystemConfigMapDto.fromJson(json[r'map'])!, | ||||||
|         metadata: SystemConfigMetadataDto.fromJson(json[r'metadata'])!, |         metadata: SystemConfigMetadataDto.fromJson(json[r'metadata'])!, | ||||||
|         newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!, |         newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!, | ||||||
|  |         nightlyTasks: SystemConfigNightlyTasksDto.fromJson(json[r'nightlyTasks'])!, | ||||||
|         notifications: SystemConfigNotificationsDto.fromJson(json[r'notifications'])!, |         notifications: SystemConfigNotificationsDto.fromJson(json[r'notifications'])!, | ||||||
|         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, |         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, | ||||||
|         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, |         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, | ||||||
| @@ -236,6 +243,7 @@ class SystemConfigDto { | |||||||
|     'map', |     'map', | ||||||
|     'metadata', |     'metadata', | ||||||
|     'newVersionCheck', |     'newVersionCheck', | ||||||
|  |     'nightlyTasks', | ||||||
|     'notifications', |     'notifications', | ||||||
|     'oauth', |     'oauth', | ||||||
|     'passwordLogin', |     'passwordLogin', | ||||||
|   | |||||||
							
								
								
									
										139
									
								
								mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | |||||||
|  | // | ||||||
|  | // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||||
|  | // | ||||||
|  | // @dart=2.18 | ||||||
|  | 
 | ||||||
|  | // 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 SystemConfigNightlyTasksDto { | ||||||
|  |   /// Returns a new [SystemConfigNightlyTasksDto] instance. | ||||||
|  |   SystemConfigNightlyTasksDto({ | ||||||
|  |     required this.clusterNewFaces, | ||||||
|  |     required this.databaseCleanup, | ||||||
|  |     required this.generateMemories, | ||||||
|  |     required this.missingThumbnails, | ||||||
|  |     required this.startTime, | ||||||
|  |     required this.syncQuotaUsage, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   bool clusterNewFaces; | ||||||
|  | 
 | ||||||
|  |   bool databaseCleanup; | ||||||
|  | 
 | ||||||
|  |   bool generateMemories; | ||||||
|  | 
 | ||||||
|  |   bool missingThumbnails; | ||||||
|  | 
 | ||||||
|  |   String startTime; | ||||||
|  | 
 | ||||||
|  |   bool syncQuotaUsage; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) => identical(this, other) || other is SystemConfigNightlyTasksDto && | ||||||
|  |     other.clusterNewFaces == clusterNewFaces && | ||||||
|  |     other.databaseCleanup == databaseCleanup && | ||||||
|  |     other.generateMemories == generateMemories && | ||||||
|  |     other.missingThumbnails == missingThumbnails && | ||||||
|  |     other.startTime == startTime && | ||||||
|  |     other.syncQuotaUsage == syncQuotaUsage; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   int get hashCode => | ||||||
|  |     // ignore: unnecessary_parenthesis | ||||||
|  |     (clusterNewFaces.hashCode) + | ||||||
|  |     (databaseCleanup.hashCode) + | ||||||
|  |     (generateMemories.hashCode) + | ||||||
|  |     (missingThumbnails.hashCode) + | ||||||
|  |     (startTime.hashCode) + | ||||||
|  |     (syncQuotaUsage.hashCode); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   String toString() => 'SystemConfigNightlyTasksDto[clusterNewFaces=$clusterNewFaces, databaseCleanup=$databaseCleanup, generateMemories=$generateMemories, missingThumbnails=$missingThumbnails, startTime=$startTime, syncQuotaUsage=$syncQuotaUsage]'; | ||||||
|  | 
 | ||||||
|  |   Map<String, dynamic> toJson() { | ||||||
|  |     final json = <String, dynamic>{}; | ||||||
|  |       json[r'clusterNewFaces'] = this.clusterNewFaces; | ||||||
|  |       json[r'databaseCleanup'] = this.databaseCleanup; | ||||||
|  |       json[r'generateMemories'] = this.generateMemories; | ||||||
|  |       json[r'missingThumbnails'] = this.missingThumbnails; | ||||||
|  |       json[r'startTime'] = this.startTime; | ||||||
|  |       json[r'syncQuotaUsage'] = this.syncQuotaUsage; | ||||||
|  |     return json; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Returns a new [SystemConfigNightlyTasksDto] instance and imports its values from | ||||||
|  |   /// [value] if it's a [Map], null otherwise. | ||||||
|  |   // ignore: prefer_constructors_over_static_methods | ||||||
|  |   static SystemConfigNightlyTasksDto? fromJson(dynamic value) { | ||||||
|  |     upgradeDto(value, "SystemConfigNightlyTasksDto"); | ||||||
|  |     if (value is Map) { | ||||||
|  |       final json = value.cast<String, dynamic>(); | ||||||
|  | 
 | ||||||
|  |       return SystemConfigNightlyTasksDto( | ||||||
|  |         clusterNewFaces: mapValueOfType<bool>(json, r'clusterNewFaces')!, | ||||||
|  |         databaseCleanup: mapValueOfType<bool>(json, r'databaseCleanup')!, | ||||||
|  |         generateMemories: mapValueOfType<bool>(json, r'generateMemories')!, | ||||||
|  |         missingThumbnails: mapValueOfType<bool>(json, r'missingThumbnails')!, | ||||||
|  |         startTime: mapValueOfType<String>(json, r'startTime')!, | ||||||
|  |         syncQuotaUsage: mapValueOfType<bool>(json, r'syncQuotaUsage')!, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static List<SystemConfigNightlyTasksDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||||
|  |     final result = <SystemConfigNightlyTasksDto>[]; | ||||||
|  |     if (json is List && json.isNotEmpty) { | ||||||
|  |       for (final row in json) { | ||||||
|  |         final value = SystemConfigNightlyTasksDto.fromJson(row); | ||||||
|  |         if (value != null) { | ||||||
|  |           result.add(value); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return result.toList(growable: growable); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static Map<String, SystemConfigNightlyTasksDto> mapFromJson(dynamic json) { | ||||||
|  |     final map = <String, SystemConfigNightlyTasksDto>{}; | ||||||
|  |     if (json is Map && json.isNotEmpty) { | ||||||
|  |       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||||
|  |       for (final entry in json.entries) { | ||||||
|  |         final value = SystemConfigNightlyTasksDto.fromJson(entry.value); | ||||||
|  |         if (value != null) { | ||||||
|  |           map[entry.key] = value; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return map; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // maps a json object with a list of SystemConfigNightlyTasksDto-objects as value to a dart map | ||||||
|  |   static Map<String, List<SystemConfigNightlyTasksDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||||
|  |     final map = <String, List<SystemConfigNightlyTasksDto>>{}; | ||||||
|  |     if (json is Map && json.isNotEmpty) { | ||||||
|  |       // ignore: parameter_assignments | ||||||
|  |       json = json.cast<String, dynamic>(); | ||||||
|  |       for (final entry in json.entries) { | ||||||
|  |         map[entry.key] = SystemConfigNightlyTasksDto.listFromJson(entry.value, growable: growable,); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return map; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// The list of required keys that must be present in a JSON. | ||||||
|  |   static const requiredKeys = <String>{ | ||||||
|  |     'clusterNewFaces', | ||||||
|  |     'databaseCleanup', | ||||||
|  |     'generateMemories', | ||||||
|  |     'missingThumbnails', | ||||||
|  |     'startTime', | ||||||
|  |     'syncQuotaUsage', | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @@ -14318,6 +14318,9 @@ | |||||||
|           "newVersionCheck": { |           "newVersionCheck": { | ||||||
|             "$ref": "#/components/schemas/SystemConfigNewVersionCheckDto" |             "$ref": "#/components/schemas/SystemConfigNewVersionCheckDto" | ||||||
|           }, |           }, | ||||||
|  |           "nightlyTasks": { | ||||||
|  |             "$ref": "#/components/schemas/SystemConfigNightlyTasksDto" | ||||||
|  |           }, | ||||||
|           "notifications": { |           "notifications": { | ||||||
|             "$ref": "#/components/schemas/SystemConfigNotificationsDto" |             "$ref": "#/components/schemas/SystemConfigNotificationsDto" | ||||||
|           }, |           }, | ||||||
| @@ -14360,6 +14363,7 @@ | |||||||
|           "map", |           "map", | ||||||
|           "metadata", |           "metadata", | ||||||
|           "newVersionCheck", |           "newVersionCheck", | ||||||
|  |           "nightlyTasks", | ||||||
|           "notifications", |           "notifications", | ||||||
|           "oauth", |           "oauth", | ||||||
|           "passwordLogin", |           "passwordLogin", | ||||||
| @@ -14790,6 +14794,37 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "object" |         "type": "object" | ||||||
|       }, |       }, | ||||||
|  |       "SystemConfigNightlyTasksDto": { | ||||||
|  |         "properties": { | ||||||
|  |           "clusterNewFaces": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "databaseCleanup": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "generateMemories": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "missingThumbnails": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "startTime": { | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "syncQuotaUsage": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "required": [ | ||||||
|  |           "clusterNewFaces", | ||||||
|  |           "databaseCleanup", | ||||||
|  |           "generateMemories", | ||||||
|  |           "missingThumbnails", | ||||||
|  |           "startTime", | ||||||
|  |           "syncQuotaUsage" | ||||||
|  |         ], | ||||||
|  |         "type": "object" | ||||||
|  |       }, | ||||||
|       "SystemConfigNotificationsDto": { |       "SystemConfigNotificationsDto": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "smtp": { |           "smtp": { | ||||||
|   | |||||||
| @@ -1389,6 +1389,14 @@ export type SystemConfigMetadataDto = { | |||||||
| export type SystemConfigNewVersionCheckDto = { | export type SystemConfigNewVersionCheckDto = { | ||||||
|     enabled: boolean; |     enabled: boolean; | ||||||
| }; | }; | ||||||
|  | export type SystemConfigNightlyTasksDto = { | ||||||
|  |     clusterNewFaces: boolean; | ||||||
|  |     databaseCleanup: boolean; | ||||||
|  |     generateMemories: boolean; | ||||||
|  |     missingThumbnails: boolean; | ||||||
|  |     startTime: string; | ||||||
|  |     syncQuotaUsage: boolean; | ||||||
|  | }; | ||||||
| export type SystemConfigNotificationsDto = { | export type SystemConfigNotificationsDto = { | ||||||
|     smtp: SystemConfigSmtpDto; |     smtp: SystemConfigSmtpDto; | ||||||
| }; | }; | ||||||
| @@ -1457,6 +1465,7 @@ export type SystemConfigDto = { | |||||||
|     map: SystemConfigMapDto; |     map: SystemConfigMapDto; | ||||||
|     metadata: SystemConfigMetadataDto; |     metadata: SystemConfigMetadataDto; | ||||||
|     newVersionCheck: SystemConfigNewVersionCheckDto; |     newVersionCheck: SystemConfigNewVersionCheckDto; | ||||||
|  |     nightlyTasks: SystemConfigNightlyTasksDto; | ||||||
|     notifications: SystemConfigNotificationsDto; |     notifications: SystemConfigNotificationsDto; | ||||||
|     oauth: SystemConfigOAuthDto; |     oauth: SystemConfigOAuthDto; | ||||||
|     passwordLogin: SystemConfigPasswordLoginDto; |     passwordLogin: SystemConfigPasswordLoginDto; | ||||||
|   | |||||||
| @@ -121,6 +121,14 @@ export interface SystemConfig { | |||||||
|   newVersionCheck: { |   newVersionCheck: { | ||||||
|     enabled: boolean; |     enabled: boolean; | ||||||
|   }; |   }; | ||||||
|  |   nightlyTasks: { | ||||||
|  |     startTime: string; | ||||||
|  |     databaseCleanup: boolean; | ||||||
|  |     missingThumbnails: boolean; | ||||||
|  |     clusterNewFaces: boolean; | ||||||
|  |     generateMemories: boolean; | ||||||
|  |     syncQuotaUsage: boolean; | ||||||
|  |   }; | ||||||
|   trash: { |   trash: { | ||||||
|     enabled: boolean; |     enabled: boolean; | ||||||
|     days: number; |     days: number; | ||||||
| @@ -298,6 +306,14 @@ export const defaults = Object.freeze<SystemConfig>({ | |||||||
|   newVersionCheck: { |   newVersionCheck: { | ||||||
|     enabled: true, |     enabled: true, | ||||||
|   }, |   }, | ||||||
|  |   nightlyTasks: { | ||||||
|  |     startTime: '00:00', | ||||||
|  |     databaseCleanup: true, | ||||||
|  |     generateMemories: true, | ||||||
|  |     syncQuotaUsage: true, | ||||||
|  |     missingThumbnails: true, | ||||||
|  |     clusterNewFaces: true, | ||||||
|  |   }, | ||||||
|   trash: { |   trash: { | ||||||
|     enabled: true, |     enabled: true, | ||||||
|     days: 30, |     days: 30, | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								server/src/controllers/system-config.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								server/src/controllers/system-config.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | import _ from 'lodash'; | ||||||
|  | import { defaults } from 'src/config'; | ||||||
|  | import { SystemConfigController } from 'src/controllers/system-config.controller'; | ||||||
|  | import { StorageTemplateService } from 'src/services/storage-template.service'; | ||||||
|  | import { SystemConfigService } from 'src/services/system-config.service'; | ||||||
|  | import request from 'supertest'; | ||||||
|  | import { errorDto } from 'test/medium/responses'; | ||||||
|  | import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; | ||||||
|  |  | ||||||
|  | describe(SystemConfigController.name, () => { | ||||||
|  |   let ctx: ControllerContext; | ||||||
|  |   const systemConfigService = mockBaseService(SystemConfigService); | ||||||
|  |   const templateService = mockBaseService(StorageTemplateService); | ||||||
|  |  | ||||||
|  |   beforeAll(async () => { | ||||||
|  |     ctx = await controllerSetup(SystemConfigController, [ | ||||||
|  |       { provide: SystemConfigService, useValue: systemConfigService }, | ||||||
|  |       { provide: StorageTemplateService, useValue: templateService }, | ||||||
|  |     ]); | ||||||
|  |     return () => ctx.close(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   beforeEach(() => { | ||||||
|  |     systemConfigService.resetAllMocks(); | ||||||
|  |     templateService.resetAllMocks(); | ||||||
|  |     ctx.reset(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('GET /system-config', () => { | ||||||
|  |     it('should be an authenticated route', async () => { | ||||||
|  |       await request(ctx.getHttpServer()).get('/system-config'); | ||||||
|  |       expect(ctx.authenticate).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('GET /system-config/defaults', () => { | ||||||
|  |     it('should be an authenticated route', async () => { | ||||||
|  |       await request(ctx.getHttpServer()).get('/system-config/defaults'); | ||||||
|  |       expect(ctx.authenticate).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('PUT /system-config', () => { | ||||||
|  |     it('should be an authenticated route', async () => { | ||||||
|  |       await request(ctx.getHttpServer()).put('/system-config'); | ||||||
|  |       expect(ctx.authenticate).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     describe('nightlyTasks', () => { | ||||||
|  |       it('should validate nightly jobs start time', async () => { | ||||||
|  |         const config = _.cloneDeep(defaults); | ||||||
|  |         config.nightlyTasks.startTime = 'invalid'; | ||||||
|  |         const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); | ||||||
|  |         expect(status).toBe(400); | ||||||
|  |         expect(body).toEqual(errorDto.badRequest(['nightlyTasks.startTime must be in HH:mm format'])); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       it('should accept a valid time', async () => { | ||||||
|  |         const config = _.cloneDeep(defaults); | ||||||
|  |         config.nightlyTasks.startTime = '05:05'; | ||||||
|  |         const { status } = await request(ctx.getHttpServer()).put('/system-config').send(config); | ||||||
|  |         expect(status).toBe(200); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       it('should validate a boolean field', async () => { | ||||||
|  |         const config = _.cloneDeep(defaults); | ||||||
|  |         (config.nightlyTasks.databaseCleanup as any) = 'invalid'; | ||||||
|  |         const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); | ||||||
|  |         expect(status).toBe(400); | ||||||
|  |         expect(body).toEqual(errorDto.badRequest(['nightlyTasks.databaseCleanup must be a boolean value'])); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -34,7 +34,7 @@ import { | |||||||
|   VideoContainer, |   VideoContainer, | ||||||
| } from 'src/enum'; | } from 'src/enum'; | ||||||
| import { ConcurrentQueueName } from 'src/types'; | import { ConcurrentQueueName } from 'src/types'; | ||||||
| import { IsCronExpression, Optional, ValidateBoolean } from 'src/validation'; | import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean } from 'src/validation'; | ||||||
|  |  | ||||||
| const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; | const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; | ||||||
| const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; | const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; | ||||||
| @@ -329,6 +329,26 @@ class SystemConfigNewVersionCheckDto { | |||||||
|   enabled!: boolean; |   enabled!: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class SystemConfigNightlyTasksDto { | ||||||
|  |   @IsDateStringFormat('HH:mm', { message: 'startTime must be in HH:mm format' }) | ||||||
|  |   startTime!: string; | ||||||
|  |  | ||||||
|  |   @ValidateBoolean() | ||||||
|  |   databaseCleanup!: boolean; | ||||||
|  |  | ||||||
|  |   @ValidateBoolean() | ||||||
|  |   missingThumbnails!: boolean; | ||||||
|  |  | ||||||
|  |   @ValidateBoolean() | ||||||
|  |   clusterNewFaces!: boolean; | ||||||
|  |  | ||||||
|  |   @ValidateBoolean() | ||||||
|  |   generateMemories!: boolean; | ||||||
|  |  | ||||||
|  |   @ValidateBoolean() | ||||||
|  |   syncQuotaUsage!: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
| class SystemConfigOAuthDto { | class SystemConfigOAuthDto { | ||||||
|   @ValidateBoolean() |   @ValidateBoolean() | ||||||
|   autoLaunch!: boolean; |   autoLaunch!: boolean; | ||||||
| @@ -638,6 +658,11 @@ export class SystemConfigDto implements SystemConfig { | |||||||
|   @IsObject() |   @IsObject() | ||||||
|   newVersionCheck!: SystemConfigNewVersionCheckDto; |   newVersionCheck!: SystemConfigNewVersionCheckDto; | ||||||
|  |  | ||||||
|  |   @Type(() => SystemConfigNightlyTasksDto) | ||||||
|  |   @ValidateNested() | ||||||
|  |   @IsObject() | ||||||
|  |   nightlyTasks!: SystemConfigNightlyTasksDto; | ||||||
|  |  | ||||||
|   @Type(() => SystemConfigOAuthDto) |   @Type(() => SystemConfigOAuthDto) | ||||||
|   @ValidateNested() |   @ValidateNested() | ||||||
|   @IsObject() |   @IsObject() | ||||||
|   | |||||||
| @@ -567,6 +567,7 @@ export enum DatabaseLock { | |||||||
|   VersionHistory = 500, |   VersionHistory = 500, | ||||||
|   CLIPDimSize = 512, |   CLIPDimSize = 512, | ||||||
|   Library = 1337, |   Library = 1337, | ||||||
|  |   NightlyJobs = 600, | ||||||
|   GetSystemConfig = 69, |   GetSystemConfig = 69, | ||||||
|   BackupDatabase = 42, |   BackupDatabase = 42, | ||||||
|   MemoryCreation = 777, |   MemoryCreation = 777, | ||||||
| @@ -684,3 +685,8 @@ export enum AssetVisibility { | |||||||
|   HIDDEN = 'hidden', |   HIDDEN = 'hidden', | ||||||
|   LOCKED = 'locked', |   LOCKED = 'locked', | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export enum CronJob { | ||||||
|  |   LibraryScan = 'LibraryScan', | ||||||
|  |   NightlyJobs = 'NightlyJobs', | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
| import { Cron, CronExpression, Interval } from '@nestjs/schedule'; | import { Interval } from '@nestjs/schedule'; | ||||||
| import { NextFunction, Request, Response } from 'express'; | import { NextFunction, Request, Response } from 'express'; | ||||||
| import { readFileSync } from 'node:fs'; | import { readFileSync } from 'node:fs'; | ||||||
| import sanitizeHtml from 'sanitize-html'; | import sanitizeHtml from 'sanitize-html'; | ||||||
| @@ -54,11 +54,6 @@ export class ApiService { | |||||||
|     await this.versionService.handleQueueVersionCheck(); |     await this.versionService.handleQueueVersionCheck(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) |  | ||||||
|   async onNightlyJob() { |  | ||||||
|     await this.jobService.handleNightlyJobs(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   ssr(excludePaths: string[]) { |   ssr(excludePaths: string[]) { | ||||||
|     const { resourcePaths } = this.configRepository.getEnv(); |     const { resourcePaths } = this.configRepository.getEnv(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -41,12 +41,12 @@ describe(JobService.name, () => { | |||||||
|         { name: JobName.USER_DELETE_CHECK }, |         { name: JobName.USER_DELETE_CHECK }, | ||||||
|         { name: JobName.PERSON_CLEANUP }, |         { name: JobName.PERSON_CLEANUP }, | ||||||
|         { name: JobName.MEMORIES_CLEANUP }, |         { name: JobName.MEMORIES_CLEANUP }, | ||||||
|         { name: JobName.MEMORIES_CREATE }, |  | ||||||
|         { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, |  | ||||||
|         { name: JobName.CLEAN_OLD_AUDIT_LOGS }, |  | ||||||
|         { name: JobName.USER_SYNC_USAGE }, |  | ||||||
|         { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }, |  | ||||||
|         { name: JobName.CLEAN_OLD_SESSION_TOKENS }, |         { name: JobName.CLEAN_OLD_SESSION_TOKENS }, | ||||||
|  |         { name: JobName.CLEAN_OLD_AUDIT_LOGS }, | ||||||
|  |         { name: JobName.MEMORIES_CREATE }, | ||||||
|  |         { name: JobName.USER_SYNC_USAGE }, | ||||||
|  |         { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, | ||||||
|  |         { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }, | ||||||
|       ]); |       ]); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { BadRequestException, Injectable } from '@nestjs/common'; | import { BadRequestException, Injectable } from '@nestjs/common'; | ||||||
| import { ClassConstructor } from 'class-transformer'; | import { ClassConstructor } from 'class-transformer'; | ||||||
| import { snakeCase } from 'lodash'; | import { snakeCase } from 'lodash'; | ||||||
|  | import { SystemConfig } from 'src/config'; | ||||||
| import { OnEvent } from 'src/decorators'; | import { OnEvent } from 'src/decorators'; | ||||||
| import { mapAsset } from 'src/dtos/asset-response.dto'; | import { mapAsset } from 'src/dtos/asset-response.dto'; | ||||||
| import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; | import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; | ||||||
| @@ -8,6 +9,8 @@ import { | |||||||
|   AssetType, |   AssetType, | ||||||
|   AssetVisibility, |   AssetVisibility, | ||||||
|   BootstrapEventPriority, |   BootstrapEventPriority, | ||||||
|  |   CronJob, | ||||||
|  |   DatabaseLock, | ||||||
|   ImmichWorker, |   ImmichWorker, | ||||||
|   JobCommand, |   JobCommand, | ||||||
|   JobName, |   JobName, | ||||||
| @@ -20,6 +23,7 @@ import { ArgOf, ArgsOf } from 'src/repositories/event.repository'; | |||||||
| import { BaseService } from 'src/services/base.service'; | import { BaseService } from 'src/services/base.service'; | ||||||
| import { ConcurrentQueueName, JobItem } from 'src/types'; | import { ConcurrentQueueName, JobItem } from 'src/types'; | ||||||
| import { hexOrBufferToBase64 } from 'src/utils/bytes'; | import { hexOrBufferToBase64 } from 'src/utils/bytes'; | ||||||
|  | import { handlePromiseError } from 'src/utils/misc'; | ||||||
|  |  | ||||||
| const asJobItem = (dto: JobCreateDto): JobItem => { | const asJobItem = (dto: JobCreateDto): JobItem => { | ||||||
|   switch (dto.name) { |   switch (dto.name) { | ||||||
| @@ -53,12 +57,59 @@ const asJobItem = (dto: JobCreateDto): JobItem => { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const asNightlyTasksCron = (config: SystemConfig) => { | ||||||
|  |   const [hours, minutes] = config.nightlyTasks.startTime.split(':').map(Number); | ||||||
|  |   return `${minutes} ${hours} * * *`; | ||||||
|  | }; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class JobService extends BaseService { | export class JobService extends BaseService { | ||||||
|   private services: ClassConstructor<unknown>[] = []; |   private services: ClassConstructor<unknown>[] = []; | ||||||
|  |   private nightlyJobsLock = false; | ||||||
|  |  | ||||||
|   @OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] }) |   @OnEvent({ name: 'config.init' }) | ||||||
|   onConfigInit({ newConfig: config }: ArgOf<'config.init'>) { |   async onConfigInit({ newConfig: config }: ArgOf<'config.init'>) { | ||||||
|  |     if (this.worker === ImmichWorker.MICROSERVICES) { | ||||||
|  |       this.updateQueueConcurrency(config); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.nightlyJobsLock = await this.databaseRepository.tryLock(DatabaseLock.NightlyJobs); | ||||||
|  |     if (this.nightlyJobsLock) { | ||||||
|  |       const cronExpression = asNightlyTasksCron(config); | ||||||
|  |       this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`); | ||||||
|  |       this.cronRepository.create({ | ||||||
|  |         name: CronJob.NightlyJobs, | ||||||
|  |         expression: cronExpression, | ||||||
|  |         start: true, | ||||||
|  |         onTick: () => handlePromiseError(this.handleNightlyJobs(), this.logger), | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @OnEvent({ name: 'config.update', server: true }) | ||||||
|  |   onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) { | ||||||
|  |     if (this.worker === ImmichWorker.MICROSERVICES) { | ||||||
|  |       this.updateQueueConcurrency(config); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this.nightlyJobsLock) { | ||||||
|  |       const cronExpression = asNightlyTasksCron(config); | ||||||
|  |       this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`); | ||||||
|  |       this.cronRepository.update({ name: CronJob.NightlyJobs, expression: cronExpression, start: true }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.JobService }) | ||||||
|  |   onBootstrap() { | ||||||
|  |     this.jobRepository.setup(this.services); | ||||||
|  |     if (this.worker === ImmichWorker.MICROSERVICES) { | ||||||
|  |       this.jobRepository.startWorkers(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private updateQueueConcurrency(config: SystemConfig) { | ||||||
|     this.logger.debug(`Updating queue concurrency settings`); |     this.logger.debug(`Updating queue concurrency settings`); | ||||||
|     for (const queueName of Object.values(QueueName)) { |     for (const queueName of Object.values(QueueName)) { | ||||||
|       let concurrency = 1; |       let concurrency = 1; | ||||||
| @@ -70,19 +121,6 @@ export class JobService extends BaseService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @OnEvent({ name: 'config.update', server: true, workers: [ImmichWorker.MICROSERVICES] }) |  | ||||||
|   onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) { |  | ||||||
|     this.onConfigInit({ newConfig: config }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.JobService }) |  | ||||||
|   onBootstrap() { |  | ||||||
|     this.jobRepository.setup(this.services); |  | ||||||
|     if (this.worker === ImmichWorker.MICROSERVICES) { |  | ||||||
|       this.jobRepository.startWorkers(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   setServices(services: ClassConstructor<unknown>[]) { |   setServices(services: ClassConstructor<unknown>[]) { | ||||||
|     this.services = services; |     this.services = services; | ||||||
|   } |   } | ||||||
| @@ -233,18 +271,37 @@ export class JobService extends BaseService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async handleNightlyJobs() { |   async handleNightlyJobs() { | ||||||
|     await this.jobRepository.queueAll([ |     const config = await this.getConfig({ withCache: false }); | ||||||
|  |     const jobs: JobItem[] = []; | ||||||
|  |  | ||||||
|  |     if (config.nightlyTasks.databaseCleanup) { | ||||||
|  |       jobs.push( | ||||||
|         { name: JobName.ASSET_DELETION_CHECK }, |         { name: JobName.ASSET_DELETION_CHECK }, | ||||||
|         { name: JobName.USER_DELETE_CHECK }, |         { name: JobName.USER_DELETE_CHECK }, | ||||||
|         { name: JobName.PERSON_CLEANUP }, |         { name: JobName.PERSON_CLEANUP }, | ||||||
|         { name: JobName.MEMORIES_CLEANUP }, |         { name: JobName.MEMORIES_CLEANUP }, | ||||||
|       { name: JobName.MEMORIES_CREATE }, |  | ||||||
|       { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, |  | ||||||
|       { name: JobName.CLEAN_OLD_AUDIT_LOGS }, |  | ||||||
|       { name: JobName.USER_SYNC_USAGE }, |  | ||||||
|       { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }, |  | ||||||
|         { name: JobName.CLEAN_OLD_SESSION_TOKENS }, |         { name: JobName.CLEAN_OLD_SESSION_TOKENS }, | ||||||
|     ]); |         { name: JobName.CLEAN_OLD_AUDIT_LOGS }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (config.nightlyTasks.generateMemories) { | ||||||
|  |       jobs.push({ name: JobName.MEMORIES_CREATE }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (config.nightlyTasks.syncQuotaUsage) { | ||||||
|  |       jobs.push({ name: JobName.USER_SYNC_USAGE }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (config.nightlyTasks.missingThumbnails) { | ||||||
|  |       jobs.push({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (config.nightlyTasks.clusterNewFaces) { | ||||||
|  |       jobs.push({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await this.jobRepository.queueAll(jobs); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { Stats } from 'node:fs'; | |||||||
| import { defaults, SystemConfig } from 'src/config'; | import { defaults, SystemConfig } from 'src/config'; | ||||||
| import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; | import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; | ||||||
| import { mapLibrary } from 'src/dtos/library.dto'; | import { mapLibrary } from 'src/dtos/library.dto'; | ||||||
| import { AssetType, ImmichWorker, JobName, JobStatus } from 'src/enum'; | import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; | ||||||
| import { LibraryService } from 'src/services/library.service'; | import { LibraryService } from 'src/services/library.service'; | ||||||
| import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types'; | import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types'; | ||||||
| import { assetStub } from 'test/fixtures/asset.stub'; | import { assetStub } from 'test/fixtures/asset.stub'; | ||||||
| @@ -56,7 +56,11 @@ describe(LibraryService.name, () => { | |||||||
|         } as SystemConfig, |         } as SystemConfig, | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       expect(mocks.cron.update).toHaveBeenCalledWith({ name: 'libraryScan', expression: '0 1 * * *', start: true }); |       expect(mocks.cron.update).toHaveBeenCalledWith({ | ||||||
|  |         name: CronJob.LibraryScan, | ||||||
|  |         expression: '0 1 * * *', | ||||||
|  |         start: true, | ||||||
|  |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should initialize watcher for all external libraries', async () => { |     it('should initialize watcher for all external libraries', async () => { | ||||||
| @@ -128,7 +132,7 @@ describe(LibraryService.name, () => { | |||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       expect(mocks.cron.update).toHaveBeenCalledWith({ |       expect(mocks.cron.update).toHaveBeenCalledWith({ | ||||||
|         name: 'libraryScan', |         name: CronJob.LibraryScan, | ||||||
|         expression: systemConfigStub.libraryScan.library.scan.cronExpression, |         expression: systemConfigStub.libraryScan.library.scan.cronExpression, | ||||||
|         start: systemConfigStub.libraryScan.library.scan.enabled, |         start: systemConfigStub.libraryScan.library.scan.enabled, | ||||||
|       }); |       }); | ||||||
| @@ -149,7 +153,7 @@ describe(LibraryService.name, () => { | |||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       expect(mocks.cron.update).toHaveBeenCalledWith({ |       expect(mocks.cron.update).toHaveBeenCalledWith({ | ||||||
|         name: 'libraryScan', |         name: CronJob.LibraryScan, | ||||||
|         expression: systemConfigStub.libraryScan.library.scan.cronExpression, |         expression: systemConfigStub.libraryScan.library.scan.cronExpression, | ||||||
|         start: systemConfigStub.libraryScan.library.scan.enabled, |         start: systemConfigStub.libraryScan.library.scan.enabled, | ||||||
|       }); |       }); | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ import { | |||||||
|   ValidateLibraryImportPathResponseDto, |   ValidateLibraryImportPathResponseDto, | ||||||
|   ValidateLibraryResponseDto, |   ValidateLibraryResponseDto, | ||||||
| } from 'src/dtos/library.dto'; | } from 'src/dtos/library.dto'; | ||||||
| import { AssetStatus, AssetType, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; | import { AssetStatus, AssetType, CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; | ||||||
| import { ArgOf } from 'src/repositories/event.repository'; | import { ArgOf } from 'src/repositories/event.repository'; | ||||||
| import { AssetSyncResult } from 'src/repositories/library.repository'; | import { AssetSyncResult } from 'src/repositories/library.repository'; | ||||||
| import { AssetTable } from 'src/schema/tables/asset.table'; | import { AssetTable } from 'src/schema/tables/asset.table'; | ||||||
| @@ -45,7 +45,7 @@ export class LibraryService extends BaseService { | |||||||
|  |  | ||||||
|     if (this.lock) { |     if (this.lock) { | ||||||
|       this.cronRepository.create({ |       this.cronRepository.create({ | ||||||
|         name: 'libraryScan', |         name: CronJob.LibraryScan, | ||||||
|         expression: scan.cronExpression, |         expression: scan.cronExpression, | ||||||
|         onTick: () => |         onTick: () => | ||||||
|           handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL }), this.logger), |           handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL }), this.logger), | ||||||
| @@ -65,7 +65,7 @@ export class LibraryService extends BaseService { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.cronRepository.update({ |     this.cronRepository.update({ | ||||||
|       name: 'libraryScan', |       name: CronJob.LibraryScan, | ||||||
|       expression: library.scan.cronExpression, |       expression: library.scan.cronExpression, | ||||||
|       start: library.scan.enabled, |       start: library.scan.enabled, | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -103,6 +103,14 @@ const updatedConfig = Object.freeze<SystemConfig>({ | |||||||
|     lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', |     lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', | ||||||
|     darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', |     darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', | ||||||
|   }, |   }, | ||||||
|  |   nightlyTasks: { | ||||||
|  |     startTime: '00:00', | ||||||
|  |     databaseCleanup: true, | ||||||
|  |     clusterNewFaces: true, | ||||||
|  |     missingThumbnails: true, | ||||||
|  |     generateMemories: true, | ||||||
|  |     syncQuotaUsage: true, | ||||||
|  |   }, | ||||||
|   reverseGeocoding: { |   reverseGeocoding: { | ||||||
|     enabled: true, |     enabled: true, | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -0,0 +1,81 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; | ||||||
|  |   import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; | ||||||
|  |   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; | ||||||
|  |   import { SettingInputFieldType } from '$lib/constants'; | ||||||
|  |   import type { SystemConfigDto } from '@immich/sdk'; | ||||||
|  |   import { isEqual } from 'lodash-es'; | ||||||
|  |   import { t } from 'svelte-i18n'; | ||||||
|  |   import { fade } from 'svelte/transition'; | ||||||
|  |   import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings'; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     savedConfig: SystemConfigDto; | ||||||
|  |     defaultConfig: SystemConfigDto; | ||||||
|  |     config: SystemConfigDto; | ||||||
|  |     disabled?: boolean; | ||||||
|  |     onReset: SettingsResetEvent; | ||||||
|  |     onSave: SettingsSaveEvent; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props(); | ||||||
|  |  | ||||||
|  |   const onsubmit = (event: Event) => { | ||||||
|  |     event.preventDefault(); | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <div class="mt-2"> | ||||||
|  |   <div in:fade={{ duration: 500 }}> | ||||||
|  |     <form autocomplete="off" {onsubmit} class="mx-4 mt-4"> | ||||||
|  |       <div class="ms-4 mt-4 flex flex-col gap-4"> | ||||||
|  |         <SettingInputField | ||||||
|  |           inputType={SettingInputFieldType.TEXT} | ||||||
|  |           label={$t('admin.nightly_tasks_start_time_setting')} | ||||||
|  |           description={$t('admin.nightly_tasks_start_time_setting_description')} | ||||||
|  |           bind:value={config.nightlyTasks.startTime} | ||||||
|  |           required={true} | ||||||
|  |           {disabled} | ||||||
|  |           isEdited={!(config.nightlyTasks.startTime === savedConfig.nightlyTasks.startTime)} | ||||||
|  |         /> | ||||||
|  |         <SettingSwitch | ||||||
|  |           title={$t('admin.nightly_tasks_database_cleanup_setting')} | ||||||
|  |           subtitle={$t('admin.nightly_tasks_database_cleanup_setting_description')} | ||||||
|  |           bind:checked={config.nightlyTasks.databaseCleanup} | ||||||
|  |           {disabled} | ||||||
|  |         /> | ||||||
|  |         <SettingSwitch | ||||||
|  |           title={$t('admin.nightly_tasks_missing_thumbnails_setting')} | ||||||
|  |           subtitle={$t('admin.nightly_tasks_missing_thumbnails_setting_description')} | ||||||
|  |           bind:checked={config.nightlyTasks.missingThumbnails} | ||||||
|  |           {disabled} | ||||||
|  |         /> | ||||||
|  |         <SettingSwitch | ||||||
|  |           title={$t('admin.nightly_tasks_cluster_new_faces_setting')} | ||||||
|  |           subtitle={$t('admin.nightly_tasks_cluster_faces_setting_description')} | ||||||
|  |           bind:checked={config.nightlyTasks.clusterNewFaces} | ||||||
|  |           {disabled} | ||||||
|  |         /> | ||||||
|  |         <SettingSwitch | ||||||
|  |           title={$t('admin.nightly_tasks_generate_memories_setting')} | ||||||
|  |           subtitle={$t('admin.nightly_tasks_generate_memories_setting_description')} | ||||||
|  |           bind:checked={config.nightlyTasks.generateMemories} | ||||||
|  |           {disabled} | ||||||
|  |         /> | ||||||
|  |         <SettingSwitch | ||||||
|  |           title={$t('admin.nightly_tasks_sync_quota_usage_setting')} | ||||||
|  |           subtitle={$t('admin.nightly_tasks_sync_quota_usage_setting_description')} | ||||||
|  |           bind:checked={config.nightlyTasks.syncQuotaUsage} | ||||||
|  |           {disabled} | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <SettingButtonsRow | ||||||
|  |         onReset={(options) => onReset({ ...options, configKeys: ['nightlyTasks'] })} | ||||||
|  |         onSave={() => onSave({ nightlyTasks: config.nightlyTasks })} | ||||||
|  |         showResetToDefault={!isEqual(savedConfig.nightlyTasks, defaultConfig.nightlyTasks)} | ||||||
|  |         {disabled} | ||||||
|  |       /> | ||||||
|  |     </form> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
| @@ -12,6 +12,7 @@ | |||||||
|   import MapSettings from '$lib/components/admin-page/settings/map-settings/map-settings.svelte'; |   import MapSettings from '$lib/components/admin-page/settings/map-settings/map-settings.svelte'; | ||||||
|   import MetadataSettings from '$lib/components/admin-page/settings/metadata-settings/metadata-settings.svelte'; |   import MetadataSettings from '$lib/components/admin-page/settings/metadata-settings/metadata-settings.svelte'; | ||||||
|   import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte'; |   import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte'; | ||||||
|  |   import NightlyTasksSettings from '$lib/components/admin-page/settings/nightly-tasks-settings/nightly-tasks-settings.svelte'; | ||||||
|   import NotificationSettings from '$lib/components/admin-page/settings/notification-settings/notification-settings.svelte'; |   import NotificationSettings from '$lib/components/admin-page/settings/notification-settings/notification-settings.svelte'; | ||||||
|   import ServerSettings from '$lib/components/admin-page/settings/server/server-settings.svelte'; |   import ServerSettings from '$lib/components/admin-page/settings/server/server-settings.svelte'; | ||||||
|   import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; |   import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; | ||||||
| @@ -33,6 +34,7 @@ | |||||||
|     mdiBackupRestore, |     mdiBackupRestore, | ||||||
|     mdiBellOutline, |     mdiBellOutline, | ||||||
|     mdiBookshelf, |     mdiBookshelf, | ||||||
|  |     mdiClockOutline, | ||||||
|     mdiContentCopy, |     mdiContentCopy, | ||||||
|     mdiDatabaseOutline, |     mdiDatabaseOutline, | ||||||
|     mdiDownload, |     mdiDownload, | ||||||
| @@ -136,13 +138,6 @@ | |||||||
|       key: 'job', |       key: 'job', | ||||||
|       icon: mdiSync, |       icon: mdiSync, | ||||||
|     }, |     }, | ||||||
|     { |  | ||||||
|       component: MetadataSettings, |  | ||||||
|       title: $t('admin.metadata_settings'), |  | ||||||
|       subtitle: $t('admin.metadata_settings_description'), |  | ||||||
|       key: 'metadata', |  | ||||||
|       icon: mdiDatabaseOutline, |  | ||||||
|     }, |  | ||||||
|     { |     { | ||||||
|       component: LibrarySettings, |       component: LibrarySettings, | ||||||
|       title: $t('admin.library_settings'), |       title: $t('admin.library_settings'), | ||||||
| @@ -171,6 +166,20 @@ | |||||||
|       key: 'location', |       key: 'location', | ||||||
|       icon: mdiMapMarkerOutline, |       icon: mdiMapMarkerOutline, | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       component: MetadataSettings, | ||||||
|  |       title: $t('admin.metadata_settings'), | ||||||
|  |       subtitle: $t('admin.metadata_settings_description'), | ||||||
|  |       key: 'metadata', | ||||||
|  |       icon: mdiDatabaseOutline, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       component: NightlyTasksSettings, | ||||||
|  |       title: $t('admin.nightly_tasks_settings'), | ||||||
|  |       subtitle: $t('admin.nightly_tasks_settings_description'), | ||||||
|  |       key: 'nightly-tasks', | ||||||
|  |       icon: mdiClockOutline, | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       component: NotificationSettings, |       component: NotificationSettings, | ||||||
|       title: $t('admin.notification_settings'), |       title: $t('admin.notification_settings'), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user