diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json
index 5c9909954c..b9f7155fe0 100644
--- a/.devcontainer/devcontainer-lock.json
+++ b/.devcontainer/devcontainer-lock.json
@@ -1,9 +1,9 @@
{
"features": {
- "ghcr.io/devcontainers/features/docker-in-docker:2": {
- "version": "2.17.0",
- "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c",
- "integrity": "sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c"
+ "ghcr.io/devcontainers/features/docker-in-docker:4": {
+ "version": "4.0.0",
+ "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:4fa87399214366e320d489991769c4f3f461e1ffe461f54eea78a41b34945bb5",
+ "integrity": "sha256:4fa87399214366e320d489991769c4f3f461e1ffe461f54eea78a41b34945bb5"
}
}
}
diff --git a/e2e/package.json b/e2e/package.json
index 7b437af114..3f342f22f2 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -26,7 +26,7 @@
"devDependencies": {
"@eslint/js": "^10.0.0",
"@faker-js/faker": "^10.1.0",
- "@futo-org/backups-orchestrator-ui": "0.12.0",
+ "@futo-org/backups-orchestrator-ui": "0.19.3",
"@immich/cli": "workspace:*",
"@immich/e2e-auth-server": "workspace:*",
"@immich/sdk": "workspace:*",
diff --git a/i18n/en.json b/i18n/en.json
index de50c5926f..d67c88abd7 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1337,6 +1337,7 @@
"main_branch_warning": "You're using a development version; we strongly recommend using a release version!",
"main_menu": "Main menu",
"maintenance_action_restore": "Restoring Database",
+ "maintenance_action_rollback": "Restoring from Backup",
"maintenance_description": "Immich has been put into maintenance mode.",
"maintenance_end": "End maintenance mode",
"maintenance_logged_in_as": "Currently logged in as {user}",
diff --git a/mise.lock b/mise.lock
index 4418f81956..c007db57eb 100644
--- a/mise.lock
+++ b/mise.lock
@@ -82,35 +82,6 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
-[tools."github:jellyfin/jellyfin-ffmpeg".options]
-asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
-
-[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
-checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
-url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
-url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
-
-[[tools."github:jellyfin/jellyfin-ffmpeg"]]
-version = "7.1.3-6"
-backend = "github:jellyfin/jellyfin-ffmpeg"
-
-[tools."github:jellyfin/jellyfin-ffmpeg".options]
-asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
-
-[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
-checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
-url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
-url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
-
-[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
-checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
-url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
-url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
-
-[[tools."github:jellyfin/jellyfin-ffmpeg"]]
-version = "7.1.3-6"
-backend = "github:jellyfin/jellyfin-ffmpeg"
-
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
@@ -124,30 +95,6 @@ checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
-[[tools."github:jellyfin/jellyfin-ffmpeg"]]
-version = "7.1.3-6"
-backend = "github:jellyfin/jellyfin-ffmpeg"
-
-[tools."github:jellyfin/jellyfin-ffmpeg".options]
-asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
-
-[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
-checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
-url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
-url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
-
-[[tools."github:jellyfin/jellyfin-ffmpeg"]]
-version = "7.1.3-6"
-backend = "github:jellyfin/jellyfin-ffmpeg"
-
-[tools."github:jellyfin/jellyfin-ffmpeg".options]
-asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
-
-[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
-checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
-url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
-url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
-
[[tools."github:webassembly/binaryen"]]
version = "version_124"
backend = "github:webassembly/binaryen"
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index a9f3c76f64..b231bd9616 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -3,13 +3,13 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
-- API version: 3.0.0-rc.2
-- Generator version: 7.22.0
+- API version: 3.0.3
+- Generator version: 7.24.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
-Dart 2.12 or later
+Dart 2.17 or later
## Installation & Usage
@@ -179,6 +179,7 @@ Class | Method | HTTP request | Description
*FilesystemApi* | [**getFileListing**](doc//FilesystemApi.md#getfilelisting) | **GET** /yucca/fs |
*IntegrationsApi* | [**configureImmichIntegration**](doc//IntegrationsApi.md#configureimmichintegration) | **POST** /yucca/integrations/immich |
*IntegrationsApi* | [**getIntegrations**](doc//IntegrationsApi.md#getintegrations) | **GET** /yucca/integrations |
+*IntegrationsApi* | [**startImmichRollback**](doc//IntegrationsApi.md#startimmichrollback) | **POST** /yucca/integrations/immich/rollback |
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs | Create a manual job
*JobsApi* | [**getQueuesLegacy**](doc//JobsApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status
*JobsApi* | [**runQueueCommandLegacy**](doc//JobsApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
@@ -503,10 +504,12 @@ Class | Method | HTTP request | Description
- [FilesystemListingResponseDto](doc//FilesystemListingResponseDto.md)
- [FoldersResponse](doc//FoldersResponse.md)
- [FoldersUpdate](doc//FoldersUpdate.md)
+ - [HlsVideoResolution](doc//HlsVideoResolution.md)
- [ImageFormat](doc//ImageFormat.md)
- [ImmichIntegrationConfigurationDto](doc//ImmichIntegrationConfigurationDto.md)
- [ImmichIntegrationDto](doc//ImmichIntegrationDto.md)
- [ImmichLibraryDto](doc//ImmichLibraryDto.md)
+ - [ImmichRollbackRequestDto](doc//ImmichRollbackRequestDto.md)
- [ImmichStateDto](doc//ImmichStateDto.md)
- [ImportRecoveryKeyRequest](doc//ImportRecoveryKeyRequest.md)
- [InspectedLocalRepositoryDto](doc//InspectedLocalRepositoryDto.md)
@@ -607,6 +610,8 @@ Class | Method | HTTP request | Description
- [RatingsUpdate](doc//RatingsUpdate.md)
- [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md)
+ - [RecentlyAddedResponse](doc//RecentlyAddedResponse.md)
+ - [RecentlyAddedUpdate](doc//RecentlyAddedUpdate.md)
- [ReleaseChannel](doc//ReleaseChannel.md)
- [ReleaseEventV1](doc//ReleaseEventV1.md)
- [ReleaseType](doc//ReleaseType.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 7b771bc599..6ca7433987 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -196,10 +196,12 @@ part 'model/filesystem_listing_item_dto.dart';
part 'model/filesystem_listing_response_dto.dart';
part 'model/folders_response.dart';
part 'model/folders_update.dart';
+part 'model/hls_video_resolution.dart';
part 'model/image_format.dart';
part 'model/immich_integration_configuration_dto.dart';
part 'model/immich_integration_dto.dart';
part 'model/immich_library_dto.dart';
+part 'model/immich_rollback_request_dto.dart';
part 'model/immich_state_dto.dart';
part 'model/import_recovery_key_request.dart';
part 'model/inspected_local_repository_dto.dart';
@@ -300,6 +302,8 @@ part 'model/ratings_response.dart';
part 'model/ratings_update.dart';
part 'model/reaction_level.dart';
part 'model/reaction_type.dart';
+part 'model/recently_added_response.dart';
+part 'model/recently_added_update.dart';
part 'model/release_channel.dart';
part 'model/release_event_v1.dart';
part 'model/release_type.dart';
diff --git a/mobile/openapi/lib/api/integrations_api.dart b/mobile/openapi/lib/api/integrations_api.dart
index 3f3667fb96..1906a0694c 100644
--- a/mobile/openapi/lib/api/integrations_api.dart
+++ b/mobile/openapi/lib/api/integrations_api.dart
@@ -97,4 +97,44 @@ class IntegrationsApi {
}
return null;
}
+
+ /// Performs an HTTP 'POST /yucca/integrations/immich/rollback' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [ImmichRollbackRequestDto] immichRollbackRequestDto (required):
+ Future startImmichRollbackWithHttpInfo(ImmichRollbackRequestDto immichRollbackRequestDto, { Future? abortTrigger, }) async {
+ // ignore: prefer_const_declarations
+ final apiPath = r'/yucca/integrations/immich/rollback';
+
+ // ignore: prefer_final_locals
+ Object? postBody = immichRollbackRequestDto;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = ['application/json'];
+
+
+ return apiClient.invokeAPI(
+ apiPath,
+ 'POST',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ abortTrigger: abortTrigger,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [ImmichRollbackRequestDto] immichRollbackRequestDto (required):
+ Future startImmichRollback(ImmichRollbackRequestDto immichRollbackRequestDto, { Future? abortTrigger, }) async {
+ final response = await startImmichRollbackWithHttpInfo(immichRollbackRequestDto, abortTrigger: abortTrigger,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ }
}
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 59ca26ec7e..60213a9b28 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -97,9 +97,9 @@ class ApiClient {
if (nullableHeaderParams != null) {
request.headers.addAll(nullableHeaderParams);
}
- if (msgBody is String) {
+ if (msgBody is String && msgBody.isNotEmpty) {
request.body = msgBody;
- } else if (msgBody is List) {
+ } else if (msgBody is List && msgBody.isNotEmpty) {
request.bodyBytes = msgBody;
} else if (msgBody is Map) {
request.bodyFields = msgBody;
@@ -417,6 +417,8 @@ class ApiClient {
return FoldersResponse.fromJson(value);
case 'FoldersUpdate':
return FoldersUpdate.fromJson(value);
+ case 'HlsVideoResolution':
+ return HlsVideoResolutionTypeTransformer().decode(value);
case 'ImageFormat':
return ImageFormatTypeTransformer().decode(value);
case 'ImmichIntegrationConfigurationDto':
@@ -425,6 +427,8 @@ class ApiClient {
return ImmichIntegrationDto.fromJson(value);
case 'ImmichLibraryDto':
return ImmichLibraryDto.fromJson(value);
+ case 'ImmichRollbackRequestDto':
+ return ImmichRollbackRequestDto.fromJson(value);
case 'ImmichStateDto':
return ImmichStateDto.fromJson(value);
case 'ImportRecoveryKeyRequest':
@@ -625,6 +629,10 @@ class ApiClient {
return ReactionLevelTypeTransformer().decode(value);
case 'ReactionType':
return ReactionTypeTypeTransformer().decode(value);
+ case 'RecentlyAddedResponse':
+ return RecentlyAddedResponse.fromJson(value);
+ case 'RecentlyAddedUpdate':
+ return RecentlyAddedUpdate.fromJson(value);
case 'ReleaseChannel':
return ReleaseChannelTypeTransformer().decode(value);
case 'ReleaseEventV1':
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index 0c21f01afb..0d8949e7c5 100644
--- a/mobile/openapi/lib/api_helper.dart
+++ b/mobile/openapi/lib/api_helper.dart
@@ -112,6 +112,9 @@ String parameterToString(dynamic value) {
if (value is Colorspace) {
return ColorspaceTypeTransformer().encode(value).toString();
}
+ if (value is HlsVideoResolution) {
+ return HlsVideoResolutionTypeTransformer().encode(value).toString();
+ }
if (value is ImageFormat) {
return ImageFormatTypeTransformer().encode(value).toString();
}
diff --git a/mobile/openapi/lib/model/backend_type.dart b/mobile/openapi/lib/model/backend_type.dart
index cacf63ae17..d056c6cdc7 100644
--- a/mobile/openapi/lib/model/backend_type.dart
+++ b/mobile/openapi/lib/model/backend_type.dart
@@ -11,31 +11,30 @@
part of openapi.api;
-class BackendType {
- /// Instantiate a new enum with the provided [value].
- const BackendType._(this.value);
+enum BackendType {
+ yucca._(r'yucca'),
+ local._(r'local'),
+ s3._(r's3'),
+ ;
+
+ /// Instantiate a new enum with the provided value.
+ const BackendType._(this._value);
/// The underlying value of this enum member.
- final String value;
+ final String _value;
@override
- String toString() => value;
+ String toString() => _value;
- String toJson() => value;
-
- static const yucca = BackendType._(r'yucca');
- static const local = BackendType._(r'local');
- static const s3 = BackendType._(r's3');
-
- /// List of all possible values in this [enum][BackendType].
- static const values = [
- yucca,
- local,
- s3,
- ];
+ /// Encodes this enum as a value suitable for JSON.
+ String toJson() => _value;
+ /// Returns the instance of [BackendType] that was successfully decoded
+ /// from the passed [value] on success, null otherwise.
static BackendType? fromJson(dynamic value) => BackendTypeTypeTransformer().decode(value);
+ /// Returns a [List] containing instances of [BackendType]
+ /// that were successfully decoded from the passed [JSON][json].
static List listFromJson(dynamic json, {bool growable = false,}) {
final result = [];
if (json is List && json.isNotEmpty) {
@@ -57,9 +56,11 @@ class BackendTypeTypeTransformer {
const BackendTypeTypeTransformer._();
- String encode(BackendType data) => data.value;
+ /// Encodes this enum as a value suitable for JSON.
+ String encode(BackendType data) => data._value;
- /// Decodes a [dynamic value][data] to a BackendType.
+ /// Returns the instance of [BackendType] that was successfully decoded
+ /// from the passed [data] value on success, null otherwise.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -68,6 +69,9 @@ class BackendTypeTypeTransformer {
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
BackendType? decode(dynamic data, {bool allowNull = true}) {
+ if (data is BackendType) {
+ return data;
+ }
if (data != null) {
switch (data) {
case r'yucca': return BackendType.yucca;
@@ -82,7 +86,7 @@ class BackendTypeTypeTransformer {
return null;
}
- /// Singleton [BackendTypeTypeTransformer] instance.
+ /// The singleton instance of this transformer.
static BackendTypeTypeTransformer? _instance;
}
diff --git a/mobile/openapi/lib/model/bootstrap_status.dart b/mobile/openapi/lib/model/bootstrap_status.dart
index 8a2ae0a7ec..991400d651 100644
--- a/mobile/openapi/lib/model/bootstrap_status.dart
+++ b/mobile/openapi/lib/model/bootstrap_status.dart
@@ -11,31 +11,30 @@
part of openapi.api;
-class BootstrapStatus {
- /// Instantiate a new enum with the provided [value].
- const BootstrapStatus._(this.value);
+enum BootstrapStatus {
+ notReady._(r'not-ready'),
+ ready._(r'ready'),
+ error._(r'error'),
+ ;
+
+ /// Instantiate a new enum with the provided value.
+ const BootstrapStatus._(this._value);
/// The underlying value of this enum member.
- final String value;
+ final String _value;
@override
- String toString() => value;
+ String toString() => _value;
- String toJson() => value;
-
- static const notReady = BootstrapStatus._(r'not-ready');
- static const ready = BootstrapStatus._(r'ready');
- static const error = BootstrapStatus._(r'error');
-
- /// List of all possible values in this [enum][BootstrapStatus].
- static const values = [
- notReady,
- ready,
- error,
- ];
+ /// Encodes this enum as a value suitable for JSON.
+ String toJson() => _value;
+ /// Returns the instance of [BootstrapStatus] that was successfully decoded
+ /// from the passed [value] on success, null otherwise.
static BootstrapStatus? fromJson(dynamic value) => BootstrapStatusTypeTransformer().decode(value);
+ /// Returns a [List] containing instances of [BootstrapStatus]
+ /// that were successfully decoded from the passed [JSON][json].
static List listFromJson(dynamic json, {bool growable = false,}) {
final result = [];
if (json is List && json.isNotEmpty) {
@@ -57,9 +56,11 @@ class BootstrapStatusTypeTransformer {
const BootstrapStatusTypeTransformer._();
- String encode(BootstrapStatus data) => data.value;
+ /// Encodes this enum as a value suitable for JSON.
+ String encode(BootstrapStatus data) => data._value;
- /// Decodes a [dynamic value][data] to a BootstrapStatus.
+ /// Returns the instance of [BootstrapStatus] that was successfully decoded
+ /// from the passed [data] value on success, null otherwise.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -68,6 +69,9 @@ class BootstrapStatusTypeTransformer {
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
BootstrapStatus? decode(dynamic data, {bool allowNull = true}) {
+ if (data is BootstrapStatus) {
+ return data;
+ }
if (data != null) {
switch (data) {
case r'not-ready': return BootstrapStatus.notReady;
@@ -82,7 +86,7 @@ class BootstrapStatusTypeTransformer {
return null;
}
- /// Singleton [BootstrapStatusTypeTransformer] instance.
+ /// The singleton instance of this transformer.
static BootstrapStatusTypeTransformer? _instance;
}
diff --git a/mobile/openapi/lib/model/immich_rollback_request_dto.dart b/mobile/openapi/lib/model/immich_rollback_request_dto.dart
new file mode 100644
index 0000000000..8c8d35263f
--- /dev/null
+++ b/mobile/openapi/lib/model/immich_rollback_request_dto.dart
@@ -0,0 +1,123 @@
+//
+// 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 ImmichRollbackRequestDto {
+ /// Returns a new [ImmichRollbackRequestDto] instance.
+ ImmichRollbackRequestDto({
+ this.backupFileName = const Optional.absent(),
+ required this.repositoryId,
+ required this.snapshotId,
+ });
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ Optional backupFileName;
+
+ String repositoryId;
+
+ String snapshotId;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is ImmichRollbackRequestDto &&
+ other.backupFileName == backupFileName &&
+ other.repositoryId == repositoryId &&
+ other.snapshotId == snapshotId;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (backupFileName == null ? 0 : backupFileName!.hashCode) +
+ (repositoryId.hashCode) +
+ (snapshotId.hashCode);
+
+ @override
+ String toString() => 'ImmichRollbackRequestDto[backupFileName=$backupFileName, repositoryId=$repositoryId, snapshotId=$snapshotId]';
+
+ Map toJson() {
+ final json = {};
+ if (this.backupFileName.isPresent) {
+ final value = this.backupFileName.value;
+ json[r'backupFileName'] = value;
+ }
+ json[r'repositoryId'] = this.repositoryId;
+ json[r'snapshotId'] = this.snapshotId;
+ return json;
+ }
+
+ /// Returns a new [ImmichRollbackRequestDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static ImmichRollbackRequestDto? fromJson(dynamic value) {
+ upgradeDto(value, "ImmichRollbackRequestDto");
+ if (value is Map) {
+ final json = value.cast();
+
+ return ImmichRollbackRequestDto(
+ backupFileName: json.containsKey(r'backupFileName') ? Optional.present(mapValueOfType(json, r'backupFileName')) : const Optional.absent(),
+ repositoryId: mapValueOfType(json, r'repositoryId')!,
+ snapshotId: mapValueOfType(json, r'snapshotId')!,
+ );
+ }
+ 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 = ImmichRollbackRequestDto.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 = ImmichRollbackRequestDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of ImmichRollbackRequestDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = ImmichRollbackRequestDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'repositoryId',
+ 'snapshotId',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/maintenance_action.dart b/mobile/openapi/lib/model/maintenance_action.dart
index d8dd6b8846..30212f79ae 100644
--- a/mobile/openapi/lib/model/maintenance_action.dart
+++ b/mobile/openapi/lib/model/maintenance_action.dart
@@ -16,6 +16,7 @@ enum MaintenanceAction {
end._(r'end'),
selectDatabaseRestore._(r'select_database_restore'),
restoreDatabase._(r'restore_database'),
+ rollback._(r'rollback'),
;
/// Instantiate a new enum with the provided value.
@@ -79,6 +80,7 @@ class MaintenanceActionTypeTransformer {
case r'end': return MaintenanceAction.end;
case r'select_database_restore': return MaintenanceAction.selectDatabaseRestore;
case r'restore_database': return MaintenanceAction.restoreDatabase;
+ case r'rollback': return MaintenanceAction.rollback;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
diff --git a/mobile/openapi/lib/model/maintenance_status_response_dto.dart b/mobile/openapi/lib/model/maintenance_status_response_dto.dart
index 82ad12d340..74b9adbcfb 100644
--- a/mobile/openapi/lib/model/maintenance_status_response_dto.dart
+++ b/mobile/openapi/lib/model/maintenance_status_response_dto.dart
@@ -18,6 +18,7 @@ class MaintenanceStatusResponseDto {
this.error = const Optional.absent(),
this.progress = const Optional.absent(),
this.task = const Optional.absent(),
+ this.yuccaLogId = const Optional.absent(),
});
MaintenanceAction action;
@@ -50,13 +51,23 @@ class MaintenanceStatusResponseDto {
///
Optional task;
+ /// Yucca log ID
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ Optional yuccaLogId;
+
@override
bool operator ==(Object other) => identical(this, other) || other is MaintenanceStatusResponseDto &&
other.action == action &&
other.active == active &&
other.error == error &&
other.progress == progress &&
- other.task == task;
+ other.task == task &&
+ other.yuccaLogId == yuccaLogId;
@override
int get hashCode =>
@@ -65,10 +76,11 @@ class MaintenanceStatusResponseDto {
(active.hashCode) +
(error == null ? 0 : error!.hashCode) +
(progress == null ? 0 : progress!.hashCode) +
- (task == null ? 0 : task!.hashCode);
+ (task == null ? 0 : task!.hashCode) +
+ (yuccaLogId == null ? 0 : yuccaLogId!.hashCode);
@override
- String toString() => 'MaintenanceStatusResponseDto[action=$action, active=$active, error=$error, progress=$progress, task=$task]';
+ String toString() => 'MaintenanceStatusResponseDto[action=$action, active=$active, error=$error, progress=$progress, task=$task, yuccaLogId=$yuccaLogId]';
Map toJson() {
final json = {};
@@ -86,6 +98,10 @@ class MaintenanceStatusResponseDto {
final value = this.task.value;
json[r'task'] = value;
}
+ if (this.yuccaLogId.isPresent) {
+ final value = this.yuccaLogId.value;
+ json[r'yuccaLogId'] = value;
+ }
return json;
}
@@ -103,6 +119,7 @@ class MaintenanceStatusResponseDto {
error: json.containsKey(r'error') ? Optional.present(mapValueOfType(json, r'error')) : const Optional.absent(),
progress: json.containsKey(r'progress') ? Optional.present(json[r'progress'] == null ? null : int.parse('${json[r'progress']}')) : const Optional.absent(),
task: json.containsKey(r'task') ? Optional.present(mapValueOfType(json, r'task')) : const Optional.absent(),
+ yuccaLogId: json.containsKey(r'yuccaLogId') ? Optional.present(mapValueOfType(json, r'yuccaLogId')) : const Optional.absent(),
);
}
return null;
diff --git a/mobile/openapi/lib/model/run_status.dart b/mobile/openapi/lib/model/run_status.dart
index 54ae25920e..212d866b04 100644
--- a/mobile/openapi/lib/model/run_status.dart
+++ b/mobile/openapi/lib/model/run_status.dart
@@ -11,31 +11,30 @@
part of openapi.api;
-class RunStatus {
- /// Instantiate a new enum with the provided [value].
- const RunStatus._(this.value);
+enum RunStatus {
+ incomplete._(r'incomplete'),
+ complete._(r'complete'),
+ failed._(r'failed'),
+ ;
+
+ /// Instantiate a new enum with the provided value.
+ const RunStatus._(this._value);
/// The underlying value of this enum member.
- final String value;
+ final String _value;
@override
- String toString() => value;
+ String toString() => _value;
- String toJson() => value;
-
- static const incomplete = RunStatus._(r'incomplete');
- static const complete = RunStatus._(r'complete');
- static const failed = RunStatus._(r'failed');
-
- /// List of all possible values in this [enum][RunStatus].
- static const values = [
- incomplete,
- complete,
- failed,
- ];
+ /// Encodes this enum as a value suitable for JSON.
+ String toJson() => _value;
+ /// Returns the instance of [RunStatus] that was successfully decoded
+ /// from the passed [value] on success, null otherwise.
static RunStatus? fromJson(dynamic value) => RunStatusTypeTransformer().decode(value);
+ /// Returns a [List] containing instances of [RunStatus]
+ /// that were successfully decoded from the passed [JSON][json].
static List listFromJson(dynamic json, {bool growable = false,}) {
final result = [];
if (json is List && json.isNotEmpty) {
@@ -57,9 +56,11 @@ class RunStatusTypeTransformer {
const RunStatusTypeTransformer._();
- String encode(RunStatus data) => data.value;
+ /// Encodes this enum as a value suitable for JSON.
+ String encode(RunStatus data) => data._value;
- /// Decodes a [dynamic value][data] to a RunStatus.
+ /// Returns the instance of [RunStatus] that was successfully decoded
+ /// from the passed [data] value on success, null otherwise.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -68,6 +69,9 @@ class RunStatusTypeTransformer {
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
RunStatus? decode(dynamic data, {bool allowNull = true}) {
+ if (data is RunStatus) {
+ return data;
+ }
if (data != null) {
switch (data) {
case r'incomplete': return RunStatus.incomplete;
@@ -82,7 +86,7 @@ class RunStatusTypeTransformer {
return null;
}
- /// Singleton [RunStatusTypeTransformer] instance.
+ /// The singleton instance of this transformer.
static RunStatusTypeTransformer? _instance;
}
diff --git a/mobile/openapi/lib/model/run_type.dart b/mobile/openapi/lib/model/run_type.dart
index b8eb404bb0..5b853b4ddb 100644
--- a/mobile/openapi/lib/model/run_type.dart
+++ b/mobile/openapi/lib/model/run_type.dart
@@ -11,33 +11,31 @@
part of openapi.api;
-class RunType {
- /// Instantiate a new enum with the provided [value].
- const RunType._(this.value);
+enum RunType {
+ schedule._(r'schedule'),
+ restore._(r'restore'),
+ backup._(r'backup'),
+ forget._(r'forget'),
+ ;
+
+ /// Instantiate a new enum with the provided value.
+ const RunType._(this._value);
/// The underlying value of this enum member.
- final String value;
+ final String _value;
@override
- String toString() => value;
+ String toString() => _value;
- String toJson() => value;
-
- static const schedule = RunType._(r'schedule');
- static const restore = RunType._(r'restore');
- static const backup = RunType._(r'backup');
- static const forget = RunType._(r'forget');
-
- /// List of all possible values in this [enum][RunType].
- static const values = [
- schedule,
- restore,
- backup,
- forget,
- ];
+ /// Encodes this enum as a value suitable for JSON.
+ String toJson() => _value;
+ /// Returns the instance of [RunType] that was successfully decoded
+ /// from the passed [value] on success, null otherwise.
static RunType? fromJson(dynamic value) => RunTypeTypeTransformer().decode(value);
+ /// Returns a [List] containing instances of [RunType]
+ /// that were successfully decoded from the passed [JSON][json].
static List listFromJson(dynamic json, {bool growable = false,}) {
final result = [];
if (json is List && json.isNotEmpty) {
@@ -59,9 +57,11 @@ class RunTypeTypeTransformer {
const RunTypeTypeTransformer._();
- String encode(RunType data) => data.value;
+ /// Encodes this enum as a value suitable for JSON.
+ String encode(RunType data) => data._value;
- /// Decodes a [dynamic value][data] to a RunType.
+ /// Returns the instance of [RunType] that was successfully decoded
+ /// from the passed [data] value on success, null otherwise.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -70,6 +70,9 @@ class RunTypeTypeTransformer {
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
RunType? decode(dynamic data, {bool allowNull = true}) {
+ if (data is RunType) {
+ return data;
+ }
if (data != null) {
switch (data) {
case r'schedule': return RunType.schedule;
@@ -85,7 +88,7 @@ class RunTypeTypeTransformer {
return null;
}
- /// Singleton [RunTypeTypeTransformer] instance.
+ /// The singleton instance of this transformer.
static RunTypeTypeTransformer? _instance;
}
diff --git a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart
index 21f123bb84..6bfd7d75ef 100644
--- a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart
+++ b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart
@@ -15,6 +15,8 @@ class SetMaintenanceModeDto {
SetMaintenanceModeDto({
required this.action,
this.restoreBackupFilename = const Optional.absent(),
+ this.rollbackRepositoryId = const Optional.absent(),
+ this.rollbackSnapshotId = const Optional.absent(),
});
MaintenanceAction action;
@@ -28,19 +30,41 @@ class SetMaintenanceModeDto {
///
Optional restoreBackupFilename;
+ /// Rollback repository ID
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ Optional rollbackRepositoryId;
+
+ /// Rollback snapshot ID
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ Optional rollbackSnapshotId;
+
@override
bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto &&
other.action == action &&
- other.restoreBackupFilename == restoreBackupFilename;
+ other.restoreBackupFilename == restoreBackupFilename &&
+ other.rollbackRepositoryId == rollbackRepositoryId &&
+ other.rollbackSnapshotId == rollbackSnapshotId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
- (restoreBackupFilename == null ? 0 : restoreBackupFilename!.hashCode);
+ (restoreBackupFilename == null ? 0 : restoreBackupFilename!.hashCode) +
+ (rollbackRepositoryId == null ? 0 : rollbackRepositoryId!.hashCode) +
+ (rollbackSnapshotId == null ? 0 : rollbackSnapshotId!.hashCode);
@override
- String toString() => 'SetMaintenanceModeDto[action=$action, restoreBackupFilename=$restoreBackupFilename]';
+ String toString() => 'SetMaintenanceModeDto[action=$action, restoreBackupFilename=$restoreBackupFilename, rollbackRepositoryId=$rollbackRepositoryId, rollbackSnapshotId=$rollbackSnapshotId]';
Map toJson() {
final json = {};
@@ -49,6 +73,14 @@ class SetMaintenanceModeDto {
final value = this.restoreBackupFilename.value;
json[r'restoreBackupFilename'] = value;
}
+ if (this.rollbackRepositoryId.isPresent) {
+ final value = this.rollbackRepositoryId.value;
+ json[r'rollbackRepositoryId'] = value;
+ }
+ if (this.rollbackSnapshotId.isPresent) {
+ final value = this.rollbackSnapshotId.value;
+ json[r'rollbackSnapshotId'] = value;
+ }
return json;
}
@@ -63,6 +95,8 @@ class SetMaintenanceModeDto {
return SetMaintenanceModeDto(
action: MaintenanceAction.fromJson(json[r'action'])!,
restoreBackupFilename: json.containsKey(r'restoreBackupFilename') ? Optional.present(mapValueOfType(json, r'restoreBackupFilename')) : const Optional.absent(),
+ rollbackRepositoryId: json.containsKey(r'rollbackRepositoryId') ? Optional.present(mapValueOfType(json, r'rollbackRepositoryId')) : const Optional.absent(),
+ rollbackSnapshotId: json.containsKey(r'rollbackSnapshotId') ? Optional.present(mapValueOfType(json, r'rollbackSnapshotId')) : const Optional.absent(),
);
}
return null;
diff --git a/mobile/openapi/lib/model/task_status.dart b/mobile/openapi/lib/model/task_status.dart
index 95aaa2acdd..692ea1027f 100644
--- a/mobile/openapi/lib/model/task_status.dart
+++ b/mobile/openapi/lib/model/task_status.dart
@@ -11,31 +11,30 @@
part of openapi.api;
-class TaskStatus {
- /// Instantiate a new enum with the provided [value].
- const TaskStatus._(this.value);
+enum TaskStatus {
+ incomplete._(r'incomplete'),
+ complete._(r'complete'),
+ failed._(r'failed'),
+ ;
+
+ /// Instantiate a new enum with the provided value.
+ const TaskStatus._(this._value);
/// The underlying value of this enum member.
- final String value;
+ final String _value;
@override
- String toString() => value;
+ String toString() => _value;
- String toJson() => value;
-
- static const incomplete = TaskStatus._(r'incomplete');
- static const complete = TaskStatus._(r'complete');
- static const failed = TaskStatus._(r'failed');
-
- /// List of all possible values in this [enum][TaskStatus].
- static const values = [
- incomplete,
- complete,
- failed,
- ];
+ /// Encodes this enum as a value suitable for JSON.
+ String toJson() => _value;
+ /// Returns the instance of [TaskStatus] that was successfully decoded
+ /// from the passed [value] on success, null otherwise.
static TaskStatus? fromJson(dynamic value) => TaskStatusTypeTransformer().decode(value);
+ /// Returns a [List] containing instances of [TaskStatus]
+ /// that were successfully decoded from the passed [JSON][json].
static List listFromJson(dynamic json, {bool growable = false,}) {
final result = [];
if (json is List && json.isNotEmpty) {
@@ -57,9 +56,11 @@ class TaskStatusTypeTransformer {
const TaskStatusTypeTransformer._();
- String encode(TaskStatus data) => data.value;
+ /// Encodes this enum as a value suitable for JSON.
+ String encode(TaskStatus data) => data._value;
- /// Decodes a [dynamic value][data] to a TaskStatus.
+ /// Returns the instance of [TaskStatus] that was successfully decoded
+ /// from the passed [data] value on success, null otherwise.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -68,6 +69,9 @@ class TaskStatusTypeTransformer {
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
TaskStatus? decode(dynamic data, {bool allowNull = true}) {
+ if (data is TaskStatus) {
+ return data;
+ }
if (data != null) {
switch (data) {
case r'incomplete': return TaskStatus.incomplete;
@@ -82,7 +86,7 @@ class TaskStatusTypeTransformer {
return null;
}
- /// Singleton [TaskStatusTypeTransformer] instance.
+ /// The singleton instance of this transformer.
static TaskStatusTypeTransformer? _instance;
}
diff --git a/mobile/openapi/lib/model/task_type.dart b/mobile/openapi/lib/model/task_type.dart
index 880aac2d39..2090749a72 100644
--- a/mobile/openapi/lib/model/task_type.dart
+++ b/mobile/openapi/lib/model/task_type.dart
@@ -11,33 +11,31 @@
part of openapi.api;
-class TaskType {
- /// Instantiate a new enum with the provided [value].
- const TaskType._(this.value);
+enum TaskType {
+ schedule._(r'schedule'),
+ restore._(r'restore'),
+ backup._(r'backup'),
+ forget._(r'forget'),
+ ;
+
+ /// Instantiate a new enum with the provided value.
+ const TaskType._(this._value);
/// The underlying value of this enum member.
- final String value;
+ final String _value;
@override
- String toString() => value;
+ String toString() => _value;
- String toJson() => value;
-
- static const schedule = TaskType._(r'schedule');
- static const restore = TaskType._(r'restore');
- static const backup = TaskType._(r'backup');
- static const forget = TaskType._(r'forget');
-
- /// List of all possible values in this [enum][TaskType].
- static const values = [
- schedule,
- restore,
- backup,
- forget,
- ];
+ /// Encodes this enum as a value suitable for JSON.
+ String toJson() => _value;
+ /// Returns the instance of [TaskType] that was successfully decoded
+ /// from the passed [value] on success, null otherwise.
static TaskType? fromJson(dynamic value) => TaskTypeTypeTransformer().decode(value);
+ /// Returns a [List] containing instances of [TaskType]
+ /// that were successfully decoded from the passed [JSON][json].
static List listFromJson(dynamic json, {bool growable = false,}) {
final result = [];
if (json is List && json.isNotEmpty) {
@@ -59,9 +57,11 @@ class TaskTypeTypeTransformer {
const TaskTypeTypeTransformer._();
- String encode(TaskType data) => data.value;
+ /// Encodes this enum as a value suitable for JSON.
+ String encode(TaskType data) => data._value;
- /// Decodes a [dynamic value][data] to a TaskType.
+ /// Returns the instance of [TaskType] that was successfully decoded
+ /// from the passed [data] value on success, null otherwise.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -70,6 +70,9 @@ class TaskTypeTypeTransformer {
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
TaskType? decode(dynamic data, {bool allowNull = true}) {
+ if (data is TaskType) {
+ return data;
+ }
if (data != null) {
switch (data) {
case r'schedule': return TaskType.schedule;
@@ -85,7 +88,7 @@ class TaskTypeTypeTransformer {
return null;
}
- /// Singleton [TaskTypeTypeTransformer] instance.
+ /// The singleton instance of this transformer.
static TaskTypeTypeTransformer? _instance;
}
diff --git a/mobile/openapi/lib/model/telemetry_level.dart b/mobile/openapi/lib/model/telemetry_level.dart
index b18b26be77..03984a0446 100644
--- a/mobile/openapi/lib/model/telemetry_level.dart
+++ b/mobile/openapi/lib/model/telemetry_level.dart
@@ -11,29 +11,29 @@
part of openapi.api;
-class TelemetryLevel {
- /// Instantiate a new enum with the provided [value].
- const TelemetryLevel._(this.value);
+enum TelemetryLevel {
+ full._(r'full'),
+ none._(r'none'),
+ ;
+
+ /// Instantiate a new enum with the provided value.
+ const TelemetryLevel._(this._value);
/// The underlying value of this enum member.
- final String value;
+ final String _value;
@override
- String toString() => value;
+ String toString() => _value;
- String toJson() => value;
-
- static const full = TelemetryLevel._(r'full');
- static const none = TelemetryLevel._(r'none');
-
- /// List of all possible values in this [enum][TelemetryLevel].
- static const values = [
- full,
- none,
- ];
+ /// Encodes this enum as a value suitable for JSON.
+ String toJson() => _value;
+ /// Returns the instance of [TelemetryLevel] that was successfully decoded
+ /// from the passed [value] on success, null otherwise.
static TelemetryLevel? fromJson(dynamic value) => TelemetryLevelTypeTransformer().decode(value);
+ /// Returns a [List] containing instances of [TelemetryLevel]
+ /// that were successfully decoded from the passed [JSON][json].
static List listFromJson(dynamic json, {bool growable = false,}) {
final result = [];
if (json is List && json.isNotEmpty) {
@@ -55,9 +55,11 @@ class TelemetryLevelTypeTransformer {
const TelemetryLevelTypeTransformer._();
- String encode(TelemetryLevel data) => data.value;
+ /// Encodes this enum as a value suitable for JSON.
+ String encode(TelemetryLevel data) => data._value;
- /// Decodes a [dynamic value][data] to a TelemetryLevel.
+ /// Returns the instance of [TelemetryLevel] that was successfully decoded
+ /// from the passed [data] value on success, null otherwise.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -66,6 +68,9 @@ class TelemetryLevelTypeTransformer {
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
TelemetryLevel? decode(dynamic data, {bool allowNull = true}) {
+ if (data is TelemetryLevel) {
+ return data;
+ }
if (data != null) {
switch (data) {
case r'full': return TelemetryLevel.full;
@@ -79,7 +84,7 @@ class TelemetryLevelTypeTransformer {
return null;
}
- /// Singleton [TelemetryLevelTypeTransformer] instance.
+ /// The singleton instance of this transformer.
static TelemetryLevelTypeTransformer? _instance;
}
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index fa5c67d0ab..5ce9bc2a80 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -16399,6 +16399,30 @@
]
}
},
+ "/yucca/integrations/immich/rollback": {
+ "post": {
+ "operationId": "startImmichRollback",
+ "parameters": [],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ImmichRollbackRequestDto"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "Integrations"
+ ]
+ }
+ },
"/yucca/logs/{id}": {
"get": {
"operationId": "getRun",
@@ -20541,6 +20565,24 @@
],
"type": "object"
},
+ "ImmichRollbackRequestDto": {
+ "properties": {
+ "backupFileName": {
+ "type": "string"
+ },
+ "repositoryId": {
+ "type": "string"
+ },
+ "snapshotId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "repositoryId",
+ "snapshotId"
+ ],
+ "type": "object"
+ },
"ImmichStateDto": {
"properties": {
"dataFolders": {
@@ -21113,7 +21155,8 @@
"start",
"end",
"select_database_restore",
- "restore_database"
+ "restore_database",
+ "rollback"
],
"type": "string"
},
@@ -21198,6 +21241,10 @@
},
"task": {
"type": "string"
+ },
+ "yuccaLogId": {
+ "description": "Yucca log ID",
+ "type": "string"
}
},
"required": [
@@ -25068,6 +25115,14 @@
"restoreBackupFilename": {
"description": "Restore backup filename",
"type": "string"
+ },
+ "rollbackRepositoryId": {
+ "description": "Rollback repository ID",
+ "type": "string"
+ },
+ "rollbackSnapshotId": {
+ "description": "Rollback snapshot ID",
+ "type": "string"
}
},
"required": [
diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts
index 243c684e2b..ea84d36e2a 100644
--- a/packages/sdk/src/fetch-client.ts
+++ b/packages/sdk/src/fetch-client.ts
@@ -93,6 +93,10 @@ export type SetMaintenanceModeDto = {
action: MaintenanceAction;
/** Restore backup filename */
restoreBackupFilename?: string;
+ /** Rollback repository ID */
+ rollbackRepositoryId?: string;
+ /** Rollback snapshot ID */
+ rollbackSnapshotId?: string;
};
export type MaintenanceDetectInstallStorageFolderDto = {
/** Number of files in the folder */
@@ -120,6 +124,8 @@ export type MaintenanceStatusResponseDto = {
error?: string;
progress?: number;
task?: string;
+ /** Yucca log ID */
+ yuccaLogId?: string;
};
export type NotificationCreateDto = {
/** Additional notification data */
@@ -2924,6 +2930,11 @@ export type ConfigureImmichIntegrationRequestDto = {
retentionPolicy?: (RetentionPolicyDto) | null;
worm: boolean;
};
+export type ImmichRollbackRequestDto = {
+ backupFileName?: string;
+ repositoryId: string;
+ snapshotId: string;
+};
export type RunDto = {
end: string;
id: string;
@@ -7387,6 +7398,15 @@ export function configureImmichIntegration({ configureImmichIntegrationRequestDt
body: configureImmichIntegrationRequestDto
})));
}
+export function startImmichRollback({ immichRollbackRequestDto }: {
+ immichRollbackRequestDto: ImmichRollbackRequestDto;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchText("/yucca/integrations/immich/rollback", oazapfts.json({
+ ...opts,
+ method: "POST",
+ body: immichRollbackRequestDto
+ })));
+}
export function getRun({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
@@ -7737,7 +7757,8 @@ export enum MaintenanceAction {
Start = "start",
End = "end",
SelectDatabaseRestore = "select_database_restore",
- RestoreDatabase = "restore_database"
+ RestoreDatabase = "restore_database",
+ Rollback = "rollback"
}
export enum StorageFolder {
EncodedVideo = "encoded-video",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1326bec337..75bbec0bf2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -112,8 +112,8 @@ importers:
specifier: ^10.1.0
version: 10.5.0
'@futo-org/backups-orchestrator-ui':
- specifier: 0.12.0
- version: 0.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))
+ specifier: 0.19.3
+ version: 0.19.3(svelte@5.56.5(@typescript-eslint/types@8.64.0))
'@immich/cli':
specifier: workspace:*
version: link:../packages/cli
@@ -404,8 +404,8 @@ importers:
specifier: 2.0.0-rc13
version: 2.0.0-rc13
'@futo-org/backups-orchestrator-api':
- specifier: 0.12.0
- version: 0.12.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(@nestjs/schedule@6.1.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)
+ specifier: 0.19.3
+ version: 0.19.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(@nestjs/schedule@6.1.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)
'@immich/plugin-sdk':
specifier: workspace:*
version: link:../packages/plugin-sdk
@@ -798,8 +798,8 @@ importers:
specifier: ^3.0.0
version: 3.5.15
'@futo-org/backups-orchestrator-ui':
- specifier: 0.12.0
- version: 0.12.0(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0))
+ specifier: 0.19.3
+ version: 0.19.3(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0))
'@immich/justified-layout-wasm':
specifier: ^0.4.3
version: 0.4.3
@@ -2907,11 +2907,11 @@ packages:
resolution: {integrity: sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==}
engines: {node: '>=6'}
- '@futo-org/backups-api-client@0.12.0':
- resolution: {integrity: sha512-DJ+XJusy8h6qCYyQUKZvY1nvLfvf8OgLIBphIV5MbcPmbBR5oLQcfyVgiyXCjWmGnbnNi8YIDiE07nerWqWfeg==}
+ '@futo-org/backups-api-client@0.19.3':
+ resolution: {integrity: sha512-aaZ/bQF2eDFrkSjRadziO6+ZrcRpdupelxwF2UusMp/WyQQCC0Qo3d59Ok5YnMxuC0iS2Kz3aDBNZqnqweBtGw==}
- '@futo-org/backups-orchestrator-api@0.12.0':
- resolution: {integrity: sha512-8wC5gbkL3yG0gZh90vh9j1IgOf6DlsT1beqVVP3qe84cIyC0yvd4GoDiBK1ymMEfXElqVSV8vV8KgjtKWb6MbA==}
+ '@futo-org/backups-orchestrator-api@0.19.3':
+ resolution: {integrity: sha512-ZEMVvtgTdwvEhiZRLVUSj4c+6vFifV0sMrXkOq1o9i19NR+jTq6BtfancJ5HnJL47zTmWHYDewgUzjMO4EqoKg==}
peerDependencies:
'@nestjs/common': ^11.0.0
'@nestjs/core': ^11.0.0
@@ -2919,8 +2919,8 @@ packages:
'@nestjs/schedule': ^6.0.0
'@nestjs/websockets': ^11.0.0
- '@futo-org/backups-orchestrator-ui@0.12.0':
- resolution: {integrity: sha512-nrGQKBdkGitvfL78P5wu0ITaOykbIQEbbUJyuLIYE6yhqH0hX6/u1WlnRoTfcXJeat3Qt+3/W4ayUYr1qOOpmA==}
+ '@futo-org/backups-orchestrator-ui@0.19.3':
+ resolution: {integrity: sha512-3fA4hTIrdESm1MqN9pddBnZndn2oAcHQPcLBslhR0TJbgeujSRl3/7jWu9q7reTeq/r4oDPxQecTLo66C1v6DQ==}
peerDependencies:
svelte: ^5.0.0
@@ -15974,13 +15974,13 @@ snapshots:
dependencies:
'@fortawesome/fontawesome-common-types': 7.2.0
- '@futo-org/backups-api-client@0.12.0':
+ '@futo-org/backups-api-client@0.19.3':
dependencies:
'@oazapfts/runtime': 1.2.0
- '@futo-org/backups-orchestrator-api@0.12.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(@nestjs/schedule@6.1.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)':
+ '@futo-org/backups-orchestrator-api@0.19.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(@nestjs/schedule@6.1.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)':
dependencies:
- '@futo-org/backups-api-client': 0.12.0
+ '@futo-org/backups-api-client': 0.19.3
'@futo-org/restic-wrapper': 1.3.0
'@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -16007,9 +16007,9 @@ snapshots:
- supports-color
- utf-8-validate
- '@futo-org/backups-orchestrator-ui@0.12.0(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0))':
+ '@futo-org/backups-orchestrator-ui@0.19.3(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0))':
dependencies:
- '@futo-org/backups-api-client': 0.12.0
+ '@futo-org/backups-api-client': 0.19.3
'@immich/ui': 0.59.0(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0))
'@mdi/js': 7.4.47
'@oazapfts/runtime': 1.2.0
@@ -16026,9 +16026,9 @@ snapshots:
- supports-color
- utf-8-validate
- '@futo-org/backups-orchestrator-ui@0.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))':
+ '@futo-org/backups-orchestrator-ui@0.19.3(svelte@5.56.5(@typescript-eslint/types@8.64.0))':
dependencies:
- '@futo-org/backups-api-client': 0.12.0
+ '@futo-org/backups-api-client': 0.19.3
'@immich/ui': 0.59.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))
'@mdi/js': 7.4.47
'@oazapfts/runtime': 1.2.0
diff --git a/server/package.json b/server/package.json
index d1abf0a47b..fa82b1b95f 100644
--- a/server/package.json
+++ b/server/package.json
@@ -37,7 +37,7 @@
},
"dependencies": {
"@extism/extism": "2.0.0-rc13",
- "@futo-org/backups-orchestrator-api": "0.12.0",
+ "@futo-org/backups-orchestrator-api": "0.19.3",
"@immich/plugin-sdk": "workspace:*",
"@immich/sql-tools": "^0.5.1",
"@nestjs/bullmq": "^11.0.1",
diff --git a/server/src/dtos/database-backup.dto.ts b/server/src/dtos/database-backup.dto.ts
index bc16f0aebf..ce623b19d2 100644
--- a/server/src/dtos/database-backup.dto.ts
+++ b/server/src/dtos/database-backup.dto.ts
@@ -27,6 +27,8 @@ const DatabaseBackupDeleteSchema = z
})
.meta({ id: 'DatabaseBackupDeleteDto' });
+export type DatabaseBackupDto = z.infer;
+
export class DatabaseBackupListResponseDto extends createZodDto(DatabaseBackupListResponseSchema) {}
export class DatabaseBackupUploadDto extends createZodDto(DatabaseBackupUploadSchema) {}
export class DatabaseBackupDeleteDto extends createZodDto(DatabaseBackupDeleteSchema) {}
diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts
index 96b376c5e6..f2446d223a 100644
--- a/server/src/dtos/maintenance.dto.ts
+++ b/server/src/dtos/maintenance.dto.ts
@@ -6,6 +6,8 @@ const SetMaintenanceModeSchema = z
.object({
action: MaintenanceActionSchema,
restoreBackupFilename: z.string().optional().describe('Restore backup filename'),
+ rollbackRepositoryId: z.string().optional().describe('Rollback repository ID'),
+ rollbackSnapshotId: z.string().optional().describe('Rollback snapshot ID'),
})
.refine(
(data) => data.action !== MaintenanceAction.RestoreDatabase || (data.restoreBackupFilename?.length ?? 0) > 0,
@@ -32,6 +34,7 @@ const MaintenanceStatusResponseSchema = z
progress: z.int().optional(),
task: z.string().optional(),
error: z.string().optional(),
+ yuccaLogId: z.string().optional().describe('Yucca log ID'),
})
.meta({ id: 'MaintenanceStatusResponseDto' });
diff --git a/server/src/enum.ts b/server/src/enum.ts
index 7b58c7ab7b..cd665c813e 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -977,6 +977,7 @@ export enum MaintenanceAction {
End = 'end',
SelectDatabaseRestore = 'select_database_restore',
RestoreDatabase = 'restore_database',
+ Rollback = 'rollback',
}
export const MaintenanceActionSchema = z
diff --git a/server/src/maintenance/maintenance-worker.service.ts b/server/src/maintenance/maintenance-worker.service.ts
index e76be43d2f..e7ad222b3a 100644
--- a/server/src/maintenance/maintenance-worker.service.ts
+++ b/server/src/maintenance/maintenance-worker.service.ts
@@ -1,4 +1,6 @@
+import { YuccaService } from '@futo-org/backups-orchestrator-api';
import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { ModuleRef } from '@nestjs/core';
import { parse } from 'cookie';
import { NextFunction, Request, Response } from 'express';
import { jwtVerify } from 'jose';
@@ -30,6 +32,7 @@ import { type ServerService as _ServerService } from 'src/services/server.servic
import { type VersionService as _VersionService } from 'src/services/version.service';
import { MaintenanceModeState } from 'src/types';
import { getConfig } from 'src/utils/config';
+import { getLatestDatabaseBackup } from 'src/utils/database-backups';
import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance';
import { getExternalDomain } from 'src/utils/misc';
import { detectMediaLocation } from 'src/utils/storage';
@@ -56,6 +59,7 @@ export class MaintenanceWorkerService {
private processRepository: ProcessRepository,
private databaseRepository: DatabaseRepository,
private databaseBackupService: DatabaseBackupService,
+ private moduleRef: ModuleRef,
) {
this.logger.setContext(this.constructor.name);
}
@@ -271,6 +275,9 @@ export class MaintenanceWorkerService {
case MaintenanceAction.RestoreDatabase: {
return this.runRestoreDatabase(action);
}
+ case MaintenanceAction.Rollback: {
+ return this.runRollback(action);
+ }
}
}
@@ -329,6 +336,67 @@ export class MaintenanceWorkerService {
});
}
+ private async runRollback(action: SetMaintenanceModeDto) {
+ const isLock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation);
+ if (!isLock) {
+ return;
+ }
+
+ this.logger.log(`Running maintenance action ${action.action}`);
+
+ await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, {
+ isMaintenanceMode: true,
+ secret: this.secret,
+ action: {
+ action: MaintenanceAction.Start,
+ },
+ });
+
+ try {
+ if (!action.rollbackRepositoryId || !action.rollbackSnapshotId) {
+ throw new Error("Expected rollbackRepositoryId and rollbackSnapshotId but they're missing!");
+ }
+
+ await this.rollback(action.rollbackRepositoryId, action.rollbackSnapshotId);
+ } catch (error) {
+ this.logger.error(`Encountered error running action: ${error}`);
+ this.setStatus({
+ active: true,
+ action: action.action,
+ task: 'error',
+ error: '' + error,
+ });
+ }
+ }
+
+ private async rollback(repositoryId: string, snapshotId: string): Promise {
+ this.setStatus({
+ active: true,
+ action: MaintenanceAction.Rollback,
+ });
+
+ const yucca = this.moduleRef.get(YuccaService, { strict: false });
+ const { logId, task } = await yucca.restoreSnapshotInplace(repositoryId, snapshotId);
+
+ this.setStatus({
+ active: true,
+ action: MaintenanceAction.Rollback,
+ yuccaLogId: logId,
+ });
+
+ await task;
+
+ const { backups } = await this.databaseBackupService.listBackups();
+ const latest = getLatestDatabaseBackup(backups);
+ if (!latest) {
+ return this.setAction({
+ action: MaintenanceAction.SelectDatabaseRestore,
+ });
+ }
+
+ await this.restoreBackup(latest.filename);
+ }
+
private async endMaintenance(): Promise {
const state: MaintenanceModeState = { isMaintenanceMode: false as const };
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state);
diff --git a/server/src/services/yucca.service.ts b/server/src/services/yucca.service.ts
index b8c953746d..aafdbb1b81 100644
--- a/server/src/services/yucca.service.ts
+++ b/server/src/services/yucca.service.ts
@@ -3,10 +3,12 @@ import { Injectable, Optional } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent } from 'src/decorators';
-import { DatabaseLock, ImmichWorker, StorageFolder } from 'src/enum';
+import { DatabaseLock, ImmichWorker, MaintenanceAction, StorageFolder } from 'src/enum';
import { DatabaseRepository } from 'src/repositories/database.repository';
import { ArgOf } from 'src/repositories/event.repository';
import { LibraryRepository } from 'src/repositories/library.repository';
+import { DatabaseBackupService } from 'src/services/database-backup.service';
+import { MaintenanceService } from 'src/services/maintenance.service';
import { getExternalDomain } from 'src/utils/misc';
@Injectable()
@@ -14,8 +16,13 @@ export class YuccaService {
constructor(
private readonly databaseRepository: DatabaseRepository,
private readonly libraryRepository: LibraryRepository,
+ private readonly databaseBackupService: DatabaseBackupService,
+ @Optional() private readonly maintenanceService: MaintenanceService,
@Optional() private readonly yuccaService: YuccaOrchestratorService,
- ) {}
+ ) {
+ this.createDatabaseBackup = this.createDatabaseBackup.bind(this);
+ this.enterMaintenanceRollback = this.enterMaintenanceRollback.bind(this);
+ }
private updateSystemConfig({ server }: SystemConfig) {
this.yuccaService.setExternalBaseUrl(getExternalDomain(server));
@@ -30,9 +37,28 @@ export class YuccaService {
libraries: libraries
.filter((library) => !library.deletedAt)
.map(({ id, name, importPaths, exclusionPatterns }) => ({ id, name, importPaths, exclusionPatterns })),
+ hooks: {
+ createDatabaseBackup: this.createDatabaseBackup,
+ enterMaintenanceRollback: this.enterMaintenanceRollback,
+ },
});
}
+ private async createDatabaseBackup() {
+ await this.databaseBackupService.createDatabaseBackup();
+ }
+
+ private enterMaintenanceRollback(repositoryId: string, snapshotId: string) {
+ return this.maintenanceService.startMaintenance(
+ {
+ action: MaintenanceAction.Rollback,
+ rollbackRepositoryId: repositoryId,
+ rollbackSnapshotId: snapshotId,
+ },
+ 'yucca-rollback',
+ );
+ }
+
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Api] })
async onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) {
this.updateSystemConfig(newConfig);
diff --git a/server/src/utils/database-backups.ts b/server/src/utils/database-backups.ts
index 70bedb32b1..4bc1f33cfd 100644
--- a/server/src/utils/database-backups.ts
+++ b/server/src/utils/database-backups.ts
@@ -1,3 +1,6 @@
+import { DateTime } from 'luxon';
+import { DatabaseBackupDto } from 'src/dtos/database-backup.dto';
+
export function isValidDatabaseBackupName(filename: string) {
return filename.match(/^[\d\w-.]+\.sql(?:\.gz)?$/);
}
@@ -17,6 +20,23 @@ export function findDatabaseBackupVersion(filename: string) {
return /-v(.*)-/.exec(filename)?.[1];
}
+function getDatabaseBackupTimestamp(backup: DatabaseBackupDto): number {
+ const dateMatch = backup.filename.match(/\d+T\d+/);
+ if (!dateMatch) {
+ return 0;
+ }
+
+ return DateTime.fromFormat(dateMatch[0], "yyyyMMdd'T'HHmmss", { zone: backup.timezone }).toMillis();
+}
+
+export function getLatestDatabaseBackup(backups: T[]): T | undefined {
+ if (backups.length === 0) {
+ return undefined;
+ }
+
+ return backups.toSorted((a, b) => getDatabaseBackupTimestamp(b) - getDatabaseBackupTimestamp(a))[0];
+}
+
export class UnsupportedPostgresError extends Error {
constructor(databaseVersion: string) {
super(`Unsupported PostgreSQL version: ${databaseVersion}`);
diff --git a/web/package.json b/web/package.json
index 6c1a71abec..e158665e2a 100644
--- a/web/package.json
+++ b/web/package.json
@@ -25,7 +25,7 @@
},
"dependencies": {
"@formatjs/icu-messageformat-parser": "^3.0.0",
- "@futo-org/backups-orchestrator-ui": "0.12.0",
+ "@futo-org/backups-orchestrator-ui": "0.19.3",
"@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "workspace:*",
"@immich/ui": "^0.83.0",
diff --git a/web/src/routes/maintenance/+page.svelte b/web/src/routes/maintenance/+page.svelte
index aadec0df0c..b58ede7e84 100644
--- a/web/src/routes/maintenance/+page.svelte
+++ b/web/src/routes/maintenance/+page.svelte
@@ -7,7 +7,7 @@
import { maintenanceStore } from '$lib/stores/maintenance.store';
import { MaintenanceAction } from '@immich/sdk';
import { Button, Heading, Link, ProgressBar, Scrollable, Text } from '@immich/ui';
- import { YuccaContext } from '@futo-org/backups-orchestrator-ui';
+ import { YuccaContext, ViewLogModal } from '@futo-org/backups-orchestrator-ui';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@@ -72,6 +72,8 @@
{$t('maintenance_task_rollback')}
{/if}
{/if}
+ {:else if $status?.action === MaintenanceAction.Rollback}
+ void 0} />
{:else if $status?.action === MaintenanceAction.SelectDatabaseRestore && $auth}
{:else}