diff --git a/data/auth.sqlite b/data/auth.sqlite index 9101e32..80243da 100644 Binary files a/data/auth.sqlite and b/data/auth.sqlite differ diff --git a/markbase-core/src/server.rs b/markbase-core/src/server.rs index 5a3b3c9..9d1c876 100644 --- a/markbase-core/src/server.rs +++ b/markbase-core/src/server.rs @@ -2753,6 +2753,7 @@ pub struct BackupConfigResponse { pub compress: String, pub encrypt: bool, pub include_checksums: bool, + pub incremental: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -2806,6 +2807,7 @@ async fn get_backup_config_handler() -> Json { compress: compress_name.to_string(), encrypt: config.encrypt, include_checksums: config.include_checksums, + incremental: config.incremental, }) } @@ -2824,6 +2826,7 @@ async fn set_backup_config_handler(Json(config): Json) -> compress, encrypt: config.encrypt, include_checksums: config.include_checksums, + incremental: config.incremental, }; scheduler.set_config(new_config); Json(serde_json::json!({"success": true, "message": "Backup config updated"})) diff --git a/markbase-core/src/vfs/backup_scheduler.rs b/markbase-core/src/vfs/backup_scheduler.rs index 9308a9a..d626209 100644 --- a/markbase-core/src/vfs/backup_scheduler.rs +++ b/markbase-core/src/vfs/backup_scheduler.rs @@ -17,6 +17,7 @@ pub struct BackupScheduleConfig { pub compress: VfsCompression, pub encrypt: bool, pub include_checksums: bool, + pub incremental: bool, } impl Default for BackupScheduleConfig { @@ -29,6 +30,7 @@ impl Default for BackupScheduleConfig { compress: VfsCompression::Zstd, encrypt: false, include_checksums: true, + incremental: true, } } } @@ -122,7 +124,12 @@ impl BackupScheduler { let snapshot_dir = self.root.join(".snapshots").join(&name); self.backend.create_dir(&snapshot_dir, 0o755)?; - self.copy_root_to_snapshot(&snapshot_dir)?; + if self.config.incremental && !self.snapshots.is_empty() { + let base_snapshot = self.snapshots.last().unwrap(); + self.copy_incremental_to_snapshot(base_snapshot, &snapshot_dir)?; + } else { + self.copy_root_to_snapshot(&snapshot_dir)?; + } if self.config.include_checksums { self.generate_checksums(&snapshot_dir)?; @@ -140,6 +147,80 @@ impl BackupScheduler { Ok(name) } + fn copy_incremental_to_snapshot(&self, base: &str, snapshot_dir: &PathBuf) -> Result<(), VfsError> { + let base_dir = self.root.join(".snapshots").join(base); + + if !self.backend.exists(&base_dir) { + return self.copy_root_to_snapshot(snapshot_dir); + } + + let entries = self.backend.read_dir(&self.root)?; + + for entry in entries { + if entry.name == ".snapshots" || entry.name == ".checksums" { + continue; + } + + let src_path = self.root.join(&entry.name); + let dst_path = snapshot_dir.join(&entry.name); + let base_path = base_dir.join(&entry.name); + + if entry.stat.is_dir { + self.copy_directory_incremental(&src_path, &dst_path, &base_path)?; + } else { + let needs_copy = !self.backend.exists(&base_path) || + self.file_changed(&src_path, &base_path)?; + + if needs_copy { + self.copy_file(&src_path, &dst_path)?; + } else { + self.create_hard_link(&base_path, &dst_path)?; + } + } + } + + Ok(()) + } + + fn file_changed(&self, src: &PathBuf, base: &PathBuf) -> Result { + let src_stat = self.backend.stat(src)?; + let base_stat = self.backend.stat(base)?; + + Ok(src_stat.size != base_stat.size || + src_stat.mtime != base_stat.mtime) + } + + fn create_hard_link(&self, src: &PathBuf, dst: &PathBuf) -> Result<(), VfsError> { + self.backend.hard_link(src, dst) + } + + fn copy_directory_incremental(&self, src: &PathBuf, dst: &PathBuf, base: &PathBuf) -> Result<(), VfsError> { + self.backend.create_dir(dst, 0o755)?; + + let entries = self.backend.read_dir(src)?; + + for entry in entries { + let child_src = src.join(&entry.name); + let child_dst = dst.join(&entry.name); + let child_base = base.join(&entry.name); + + if entry.stat.is_dir { + self.copy_directory_incremental(&child_src, &child_dst, &child_base)?; + } else { + let needs_copy = !self.backend.exists(&child_base) || + self.file_changed(&child_src, &child_base)?; + + if needs_copy { + self.copy_file(&child_src, &child_dst)?; + } else { + self.create_hard_link(&child_base, &child_dst)?; + } + } + } + + Ok(()) + } + fn copy_root_to_snapshot(&self, snapshot_dir: &PathBuf) -> Result<(), VfsError> { let entries = self.backend.read_dir(&self.root)?; diff --git a/markbase-tauri/src/src/views/Backup.vue b/markbase-tauri/src/src/views/Backup.vue index 7f83923..b72a5f9 100644 --- a/markbase-tauri/src/src/views/Backup.vue +++ b/markbase-tauri/src/src/views/Backup.vue @@ -28,7 +28,11 @@ const backupConfig = ref({ enabled: false, interval_hours: 24, max_snapshots: 7, - auto_cleanup: true + auto_cleanup: true, + compress: 'zstd', + encrypt: false, + include_checksums: true, + incremental: true }) const schedulerStats = ref({ @@ -374,6 +378,19 @@ onMounted(async () => { + + + + Only copy changed files (hardlink unchanged) + + + + + + + + + Save Config