From e91ba2ae74321770b3449af1e0524eec4903dea2 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Sat, 3 Oct 2026 09:12:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(backup):=20=E5=AE=9E=E4=BE=8B=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E7=AC=AC1=E8=BD=AE=E2=80=94=E2=80=94=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E5=A4=87=E4=BB=BD=E5=BA=93=E8=BF=81=E7=A7=BB=E3=80=81?= =?UTF-8?q?settings=20=E4=BB=93=E5=BA=93=E8=B7=AF=E5=BE=84=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E3=80=81=E5=A4=87=E4=BB=BD=E8=BF=9B=E5=BA=A6=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260920131000_initial.sql | 76 +++++++++++++++++++ .../20260920150000_exclusion_rules.sql | 19 +++++ .../20260920200000_operations.sql | 26 +++++++ .../20260920210000_operation_cancellation.sql | 2 + ...60920223000_repository_move_operations.sql | 45 +++++++++++ ...60920230000_restore_preview_operations.sql | 49 ++++++++++++ ...030000_instance-backup-repository-path.sql | 1 + ...100_pending-backup-repository-cleanups.sql | 4 + packages/app-lib/src/event/emit.rs | 18 ++++- packages/app-lib/src/event/mod.rs | 51 +++++++++++++ packages/app-lib/src/state/settings.rs | 8 ++ 11 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 packages/app-lib/backup_migrations/20260920131000_initial.sql create mode 100644 packages/app-lib/backup_migrations/20260920150000_exclusion_rules.sql create mode 100644 packages/app-lib/backup_migrations/20260920200000_operations.sql create mode 100644 packages/app-lib/backup_migrations/20260920210000_operation_cancellation.sql create mode 100644 packages/app-lib/backup_migrations/20260920223000_repository_move_operations.sql create mode 100644 packages/app-lib/backup_migrations/20260920230000_restore_preview_operations.sql create mode 100644 packages/app-lib/migrations/20261002030000_instance-backup-repository-path.sql create mode 100644 packages/app-lib/migrations/20261002030100_pending-backup-repository-cleanups.sql diff --git a/packages/app-lib/backup_migrations/20260920131000_initial.sql b/packages/app-lib/backup_migrations/20260920131000_initial.sql new file mode 100644 index 0000000..ef4fb27 --- /dev/null +++ b/packages/app-lib/backup_migrations/20260920131000_initial.sql @@ -0,0 +1,76 @@ +CREATE TABLE repository_meta ( + id INTEGER PRIMARY KEY CHECK (id = 0), + schema_name TEXT NOT NULL, + created_at INTEGER NOT NULL +); + +INSERT INTO repository_meta (id, schema_name, created_at) +VALUES (0, 'axolotl-instance-backups', unixepoch('subsec') * 1000); + +CREATE TABLE instance_backup_configs ( + instance_id TEXT PRIMARY KEY, + instance_name TEXT NOT NULL, + created_at INTEGER NOT NULL, + modified_at INTEGER NOT NULL +); + +CREATE TABLE backup_selections ( + instance_id TEXT NOT NULL REFERENCES instance_backup_configs(instance_id) ON DELETE CASCADE, + path TEXT NOT NULL, + PRIMARY KEY (instance_id, path) +); + +CREATE TABLE backup_snapshots ( + id TEXT PRIMARY KEY, + instance_id TEXT NOT NULL REFERENCES instance_backup_configs(instance_id) ON DELETE CASCADE, + instance_name TEXT NOT NULL, + created_at INTEGER NOT NULL, + file_count INTEGER NOT NULL, + symlink_count INTEGER NOT NULL, + logical_size INTEGER NOT NULL, + added_size INTEGER NOT NULL +); + +CREATE INDEX backup_snapshots_instance_created + ON backup_snapshots(instance_id, created_at DESC); + +CREATE TABLE snapshot_roots ( + snapshot_id TEXT NOT NULL REFERENCES backup_snapshots(id) ON DELETE CASCADE, + path TEXT NOT NULL, + existed INTEGER NOT NULL, + PRIMARY KEY (snapshot_id, path) +); + +CREATE TABLE backup_objects ( + hash TEXT PRIMARY KEY, + size INTEGER NOT NULL, + ref_count INTEGER NOT NULL CHECK (ref_count >= 0) +); + +CREATE TABLE snapshot_entries ( + snapshot_id TEXT NOT NULL REFERENCES backup_snapshots(id) ON DELETE CASCADE, + path TEXT NOT NULL, + kind TEXT NOT NULL CHECK (kind IN ('file', 'directory', 'symlink')), + object_hash TEXT REFERENCES backup_objects(hash), + size INTEGER NOT NULL DEFAULT 0, + modified_at INTEGER, + unix_mode INTEGER, + link_target TEXT, + link_is_directory INTEGER, + PRIMARY KEY (snapshot_id, path), + CHECK ((kind = 'file' AND object_hash IS NOT NULL) OR (kind != 'file' AND object_hash IS NULL)), + CHECK ((kind = 'symlink' AND link_target IS NOT NULL) OR (kind != 'symlink' AND link_target IS NULL)) +); + +CREATE INDEX snapshot_entries_object_hash + ON snapshot_entries(object_hash) WHERE object_hash IS NOT NULL; + +CREATE TABLE object_gc_queue ( + hash TEXT PRIMARY KEY, + queued_at INTEGER NOT NULL +); + +CREATE TABLE pending_instance_deletions ( + instance_id TEXT PRIMARY KEY, + marked_at INTEGER NOT NULL +); diff --git a/packages/app-lib/backup_migrations/20260920150000_exclusion_rules.sql b/packages/app-lib/backup_migrations/20260920150000_exclusion_rules.sql new file mode 100644 index 0000000..61fb9b8 --- /dev/null +++ b/packages/app-lib/backup_migrations/20260920150000_exclusion_rules.sql @@ -0,0 +1,19 @@ +ALTER TABLE backup_snapshots + ADD COLUMN scope_mode TEXT NOT NULL DEFAULT 'included_roots' + CHECK (scope_mode IN ('included_roots', 'excluded_paths')); + +DROP TABLE backup_selections; + +CREATE TABLE backup_exclusions ( + instance_id TEXT NOT NULL REFERENCES instance_backup_configs(instance_id) ON DELETE CASCADE, + path TEXT NOT NULL, + kind TEXT NOT NULL CHECK (kind IN ('file', 'directory')), + PRIMARY KEY (instance_id, path) +); + +CREATE TABLE snapshot_exclusions ( + snapshot_id TEXT NOT NULL REFERENCES backup_snapshots(id) ON DELETE CASCADE, + path TEXT NOT NULL, + kind TEXT NOT NULL CHECK (kind IN ('file', 'directory')), + PRIMARY KEY (snapshot_id, path) +); diff --git a/packages/app-lib/backup_migrations/20260920200000_operations.sql b/packages/app-lib/backup_migrations/20260920200000_operations.sql new file mode 100644 index 0000000..048ac4c --- /dev/null +++ b/packages/app-lib/backup_migrations/20260920200000_operations.sql @@ -0,0 +1,26 @@ +CREATE TABLE backup_operations ( + id TEXT PRIMARY KEY, + operation_type TEXT NOT NULL + CHECK (operation_type IN ('create', 'restore')), + instance_id TEXT NOT NULL, + snapshot_id TEXT, + state TEXT NOT NULL + CHECK (state IN ( + 'queued', 'scanning', 'hashing', 'saving', 'validating', + 'materializing', 'applying', 'completed', 'cancelled', 'failed', 'interrupted' + )), + processed_bytes INTEGER NOT NULL DEFAULT 0, + total_bytes INTEGER NOT NULL DEFAULT 0, + cancellable INTEGER NOT NULL DEFAULT 1, + error TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + finished_at INTEGER +); + +CREATE INDEX backup_operations_instance_created + ON backup_operations(instance_id, created_at DESC); + +CREATE INDEX backup_operations_active + ON backup_operations(state, updated_at DESC) + WHERE state NOT IN ('completed', 'cancelled', 'failed', 'interrupted'); diff --git a/packages/app-lib/backup_migrations/20260920210000_operation_cancellation.sql b/packages/app-lib/backup_migrations/20260920210000_operation_cancellation.sql new file mode 100644 index 0000000..0b0fc53 --- /dev/null +++ b/packages/app-lib/backup_migrations/20260920210000_operation_cancellation.sql @@ -0,0 +1,2 @@ +ALTER TABLE backup_operations + ADD COLUMN cancel_requested INTEGER NOT NULL DEFAULT 0; diff --git a/packages/app-lib/backup_migrations/20260920223000_repository_move_operations.sql b/packages/app-lib/backup_migrations/20260920223000_repository_move_operations.sql new file mode 100644 index 0000000..e6da14e --- /dev/null +++ b/packages/app-lib/backup_migrations/20260920223000_repository_move_operations.sql @@ -0,0 +1,45 @@ +PRAGMA foreign_keys = OFF; + +CREATE TABLE backup_operations_new ( + id TEXT PRIMARY KEY, + operation_type TEXT NOT NULL + CHECK (operation_type IN ('create', 'restore', 'repository_move')), + instance_id TEXT NOT NULL, + snapshot_id TEXT, + state TEXT NOT NULL + CHECK (state IN ( + 'queued', 'scanning', 'hashing', 'saving', 'validating', + 'materializing', 'applying', 'completed', 'cancelled', 'failed', 'interrupted' + )), + processed_bytes INTEGER NOT NULL DEFAULT 0, + total_bytes INTEGER NOT NULL DEFAULT 0, + cancellable INTEGER NOT NULL DEFAULT 1, + error TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + finished_at INTEGER, + cancel_requested INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO backup_operations_new ( + id, operation_type, instance_id, snapshot_id, state, + processed_bytes, total_bytes, cancellable, error, + created_at, updated_at, finished_at, cancel_requested +) +SELECT + id, operation_type, instance_id, snapshot_id, state, + processed_bytes, total_bytes, cancellable, error, + created_at, updated_at, finished_at, cancel_requested +FROM backup_operations; + +DROP TABLE backup_operations; +ALTER TABLE backup_operations_new RENAME TO backup_operations; + +CREATE INDEX backup_operations_instance_created + ON backup_operations(instance_id, created_at DESC); + +CREATE INDEX backup_operations_active + ON backup_operations(state, updated_at DESC) + WHERE state NOT IN ('completed', 'cancelled', 'failed', 'interrupted'); + +PRAGMA foreign_keys = ON; diff --git a/packages/app-lib/backup_migrations/20260920230000_restore_preview_operations.sql b/packages/app-lib/backup_migrations/20260920230000_restore_preview_operations.sql new file mode 100644 index 0000000..0a0fc22 --- /dev/null +++ b/packages/app-lib/backup_migrations/20260920230000_restore_preview_operations.sql @@ -0,0 +1,49 @@ +PRAGMA foreign_keys = OFF; + +CREATE TABLE backup_operations_new ( + id TEXT PRIMARY KEY, + operation_type TEXT NOT NULL + CHECK (operation_type IN ('create', 'restore', 'repository_move', 'restore_preview')), + instance_id TEXT NOT NULL, + snapshot_id TEXT, + state TEXT NOT NULL + CHECK (state IN ( + 'queued', 'scanning', 'hashing', 'saving', 'validating', + 'materializing', 'applying', 'completed', 'cancelled', 'failed', 'interrupted' + )), + processed_bytes INTEGER NOT NULL DEFAULT 0, + total_bytes INTEGER NOT NULL DEFAULT 0, + cancellable INTEGER NOT NULL DEFAULT 1, + error TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + finished_at INTEGER, + cancel_requested INTEGER NOT NULL DEFAULT 0, + preview_added_files INTEGER, + preview_modified_files INTEGER, + preview_deleted_files INTEGER, + preview_plan_token TEXT +); + +INSERT INTO backup_operations_new ( + id, operation_type, instance_id, snapshot_id, state, + processed_bytes, total_bytes, cancellable, error, + created_at, updated_at, finished_at, cancel_requested +) +SELECT + id, operation_type, instance_id, snapshot_id, state, + processed_bytes, total_bytes, cancellable, error, + created_at, updated_at, finished_at, cancel_requested +FROM backup_operations; + +DROP TABLE backup_operations; +ALTER TABLE backup_operations_new RENAME TO backup_operations; + +CREATE INDEX backup_operations_instance_created + ON backup_operations(instance_id, created_at DESC); + +CREATE INDEX backup_operations_active + ON backup_operations(state, updated_at DESC) + WHERE state NOT IN ('completed', 'cancelled', 'failed', 'interrupted'); + +PRAGMA foreign_keys = ON; diff --git a/packages/app-lib/migrations/20261002030000_instance-backup-repository-path.sql b/packages/app-lib/migrations/20261002030000_instance-backup-repository-path.sql new file mode 100644 index 0000000..8b87613 --- /dev/null +++ b/packages/app-lib/migrations/20261002030000_instance-backup-repository-path.sql @@ -0,0 +1 @@ +ALTER TABLE settings ADD COLUMN backup_repository_path TEXT; diff --git a/packages/app-lib/migrations/20261002030100_pending-backup-repository-cleanups.sql b/packages/app-lib/migrations/20261002030100_pending-backup-repository-cleanups.sql new file mode 100644 index 0000000..dd21092 --- /dev/null +++ b/packages/app-lib/migrations/20261002030100_pending-backup-repository-cleanups.sql @@ -0,0 +1,4 @@ +CREATE TABLE pending_backup_repository_cleanups ( + path TEXT PRIMARY KEY, + created_at INTEGER NOT NULL +); diff --git a/packages/app-lib/src/event/emit.rs b/packages/app-lib/src/event/emit.rs index 1c533d5..0308da3 100644 --- a/packages/app-lib/src/event/emit.rs +++ b/packages/app-lib/src/event/emit.rs @@ -1,6 +1,7 @@ use super::{FriendPayload, LoadingBarId}; use crate::event::{ - CommandPayload, EventError, InstanceBulkUpdateProgressPayload, + CommandPayload, EventError, InstanceBackupProgressPayload, + InstanceBulkUpdateProgressPayload, InstancePayloadType, LoadingBar, LoadingBarType, ProcessPayloadType, ServerPayloadType, }; @@ -358,6 +359,21 @@ pub async fn emit_instance_bulk_update_progress( Ok(()) } +#[allow(unused_variables)] +pub async fn emit_instance_backup_progress( + payload: InstanceBackupProgressPayload, +) -> crate::Result<()> { + #[cfg(feature = "tauri")] + { + let event_state = crate::EventState::get()?; + event_state + .app + .emit("instance_backup_progress", payload) + .map_err(EventError::from)?; + } + Ok(()) +} + // emit_command(CommandPayload::Something { something }) // ie: installing a pack, opening an .mrpack, etc // Generally used for url deep links and file opens that we want to handle in the frontend diff --git a/packages/app-lib/src/event/mod.rs b/packages/app-lib/src/event/mod.rs index 5532814..a12e48f 100644 --- a/packages/app-lib/src/event/mod.rs +++ b/packages/app-lib/src/event/mod.rs @@ -248,6 +248,57 @@ pub enum InstanceBulkUpdateProgressStage { Finishing, } +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InstanceBackupProgressPayload { + pub operation_id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub instance_id: Option, + pub operation_type: InstanceBackupOperationType, + pub stage: InstanceBackupProgressStage, + pub processed_bytes: u64, + pub total_bytes: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub final_state: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +#[derive(Serialize, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum InstanceBackupOperationType { + Create, + Restore, + Delete, + RepositoryMove, + RestorePreview, +} + +#[derive(Serialize, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum InstanceBackupOperationFinalState { + Completed, + Cancelled, + Failed, +} + +#[derive(Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum InstanceBackupProgressStage { + Scanning, + Hashing, + Saving, + Validating, + Copying, + Restoring, + Deleting, + Completed, + Cancelled, + Failed, +} + #[derive(Serialize, Deserialize, Clone)] #[serde(tag = "event")] pub enum CommandPayload { diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs index 053541e..edf4da7 100644 --- a/packages/app-lib/src/state/settings.rs +++ b/packages/app-lib/src/state/settings.rs @@ -147,6 +147,8 @@ pub struct Settings { #[serde(default)] pub ui_font: Option, #[serde(default)] + pub backup_repository_path: Option, + #[serde(default)] pub mono_font: Option, #[serde(default)] pub close_behavior: String, @@ -294,6 +296,7 @@ impl Settings { json(terracotta_public_nodes) terracotta_public_nodes, ui_scale, ui_font, + backup_repository_path, mono_font, version FROM settings @@ -369,6 +372,7 @@ impl Settings { sidebar_instance_count: res.sidebar_instance_count as u32, ui_scale: res.ui_scale, ui_font: res.ui_font, + backup_repository_path: res.backup_repository_path, mono_font: res.mono_font, close_behavior, auto_hide_downloads_button: res.auto_hide_downloads_button == 1, @@ -677,6 +681,10 @@ impl Settings { .bind(&self.ui_font) .execute(exec) .await?; + sqlx::query("UPDATE settings SET backup_repository_path = ? WHERE id = 0") + .bind(&self.backup_repository_path) + .execute(exec) + .await?; sqlx::query("UPDATE settings SET mono_font = ? WHERE id = 0") .bind(&self.mono_font) .execute(exec)