From 467d86f53d64b072146824ec4bdadebf6ac95049 Mon Sep 17 00:00:00 2001 From: Robin Gottschalk Date: Sun, 2 Feb 2025 10:59:45 +0100 Subject: [PATCH] Implement file syncing to a seperate folder with the git repository --- README.md | 8 +++- src/filesync.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ src/gitpush.rs | 8 ++++ src/main.rs | 51 ++++++++++++++++++-------- 4 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 src/filesync.rs create mode 100644 src/gitpush.rs diff --git a/README.md b/README.md index 463395a..0678354 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,15 @@ This script automates the process of backing up your Obsidian vault to a Git rep ## Features - [x] Trigger backup after file changes with delay +- [x] Maintain git repo in a seperate folder to not have the repo synced by syncthing (copy changed files over) - [ ] Push changes to remote repository -- [ ] Maintain git repo in a seperate folder to not have the repo synced by syncthing (copy changed files over) +- [ ] Some error management - [x] Tray Menu - [x] Exit - [x] Backup now - [ ] See current status (Time after last file change, backup in progress) + +## Installation + +- Create target repo folder and initialize an empty git repository +- Link repository to remote and set upstream branch (git push working) diff --git a/src/filesync.rs b/src/filesync.rs new file mode 100644 index 0000000..8f33e48 --- /dev/null +++ b/src/filesync.rs @@ -0,0 +1,97 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +pub fn sync_directory( + source: &Path, + target: &Path, + protect: Option<&str>, // Will not get touched in target (file or directory at top level) +) -> Result<(), Box> { + // Ensure the target directory exists + fs::create_dir_all(target)?; + + // Gather all paths in the source and target directories + let filter = |entry: Result| { + entry.ok().and_then(|entry| { + if entry.file_name().to_str() != protect { + Some(entry.path()) + } else { + None + } + }) + }; + let mut source_entries: Vec = fs::read_dir(source)?.filter_map(filter).collect(); + let mut target_entries: Vec = fs::read_dir(target)?.filter_map(filter).collect(); + + // Sort entries to ensure consistent order for comparison + source_entries.sort_unstable(); + target_entries.sort_unstable(); + + // Two-pointer technique: iterate through both directories simultaneously + let mut i = 0; + let mut j = 0; + + while i < source_entries.len() || j < target_entries.len() { + if i == source_entries.len() { + // If there are remaining entries in the target, remove them + remove_entry(&target_entries[j])?; + j += 1; + } else if j == target_entries.len() { + // If there are remaining entries in the source, copy them to the target + sync_entry(&source_entries[i], target)?; + i += 1; + } else { + let source_path = &source_entries[i]; + let target_path = &target_entries[j]; + + let source_name = source_path.file_name().unwrap(); + let target_name = target_path.file_name().unwrap(); + + if source_name == target_name { + // If both entries are the same, sync them + sync_entry(source_path, target)?; + i += 1; + j += 1; + } else if source_name < target_name { + // If the source entry is less than the target entry, copy it to the target + sync_entry(source_path, target)?; + i += 1; + } else { + // If the target entry is less than the source entry, remove it from the target + remove_entry(target_path)?; + j += 1; + } + } + } + + Ok(()) +} + +fn sync_entry(source: &Path, target: &Path) -> Result<(), Box> { + let target_path = target.join(source.file_name().unwrap()); + + if source.is_file() { + // Copy the file if it doesn't exist or differs + if !target_path.exists() + || fs::metadata(source)?.modified()? != fs::metadata(&target_path)?.modified()? + { + println!("File copied/replaced: {:?}", source); + fs::create_dir_all(target_path.parent().unwrap())?; + fs::copy(source, &target_path)?; + } + } else if source.is_dir() { + // Recursively sync directories + sync_directory(source, &target_path, None)?; + } + + Ok(()) +} + +fn remove_entry(path: &Path) -> Result<(), Box> { + println!("File/dir removed: {:?}", path); + if path.is_file() { + fs::remove_file(path)?; + } else if path.is_dir() { + fs::remove_dir_all(path)?; + } + Ok(()) +} diff --git a/src/gitpush.rs b/src/gitpush.rs new file mode 100644 index 0000000..2f665a1 --- /dev/null +++ b/src/gitpush.rs @@ -0,0 +1,8 @@ +pub fn backup_changes() { + let start = std::time::Instant::now(); + + println!("Checking for changes..."); + // TODO log changed files first + + println!("Backup took {:?}", start.elapsed()); +} diff --git a/src/main.rs b/src/main.rs index 2791100..05a6d3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use notify::{Event, RecursiveMode, Watcher}; + use std::path::Path; use std::time::{Duration, Instant}; use tray_icon::{ @@ -12,6 +13,13 @@ use winit::{ event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, }; +mod filesync; +mod gitpush; + +const SOURCE_VAULT: &str = "D:\\test\\obsidian-vault-source"; // syncthing vault folder +const REPO_VAULT: &str = "D:\\test\\obsidian-vault-test"; // git repo folder + +#[derive(Debug)] enum UserEvent { MenuEvent(tray_icon::menu::MenuEvent), FileEvent(Event), @@ -36,7 +44,7 @@ impl ApplicationHandler for App { tray_menu.append(&menu_item_backup_now).unwrap(); tray_menu.append(&menu_item_exit).unwrap(); - let icon_path = Path::new("D:/Dev/obsidian-git-backup/sync.ico"); + let icon_path = Path::new(".\\sync.ico"); let icon = Icon::from_path(icon_path, None).unwrap(); let tray_icon = TrayIconBuilder::new() .with_menu(Box::new(tray_menu)) @@ -59,11 +67,13 @@ impl ApplicationHandler for App { UserEvent::MenuEvent(menu_event) => { match menu_event.id() { id if id == self.menu_backup_now_id.as_ref().unwrap() => { + println!("---> Backup triggered by menu"); // Handle push now menu item self.last_change_time = None; - backup_changes(); + gitpush::backup_changes(); } id if id == self.menu_exit_id.as_ref().unwrap() => { + println!("---> Exit triggert by menu"); // Handle exit menu item event_loop.exit(); } @@ -71,9 +81,14 @@ impl ApplicationHandler for App { } } UserEvent::FileEvent(_event) => { - println!("File change detected"); + println!("---> File change detected"); self.last_change_time = Some(Instant::now()); - println!("At: {:?}", self.last_change_time.unwrap()); + let source = Path::new(SOURCE_VAULT); + let target = Path::new(REPO_VAULT); + let start = std::time::Instant::now(); + filesync::sync_directory(source, target, Some(".git")) + .expect("Directories could not be synced"); + println!("Sync took {:?}", start.elapsed()); } } } @@ -84,8 +99,9 @@ impl ApplicationHandler for App { // Backup changes if last file change time exceeded threshold if let Some(last_change_time) = self.last_change_time { if now.duration_since(last_change_time) > self.duration_threshold { + println!("---> Backup triggered by file change"); self.last_change_time = None; - backup_changes(); + gitpush::backup_changes(); } } @@ -102,33 +118,38 @@ impl ApplicationHandler for App { } } -fn backup_changes() { - println!("Backup changes started"); - println!("Checking for changes..."); -} - fn main() { + // Sync folder and check for changes to back up + let source_dir = Path::new(SOURCE_VAULT); + let repo_dir = Path::new(REPO_VAULT); + let start = std::time::Instant::now(); + filesync::sync_directory(source_dir, repo_dir, Some(".git")) + .expect("Directories could not be synced"); + println!("Sync took {:?}", start.elapsed()); + gitpush::backup_changes(); + + // Create event loop let event_loop = EventLoop::::with_user_event().build().unwrap(); event_loop.set_control_flow(ControlFlow::Wait); // Set up menu event handler let proxy = event_loop.create_proxy(); tray_icon::menu::MenuEvent::set_event_handler(Some(move |event| { - let _ = proxy.send_event(UserEvent::MenuEvent(event)); + proxy.send_event(UserEvent::MenuEvent(event)).unwrap(); })); // Set up file change event handler let proxy = event_loop.create_proxy(); - let repo_path = Path::new("D:/test/obsidian-vault-test"); let mut watcher = notify::recommended_watcher(move |event| { if let Ok(event) = event { - let _ = proxy.send_event(UserEvent::FileEvent(event)); + proxy.send_event(UserEvent::FileEvent(event)).unwrap(); } }) .unwrap(); - watcher.watch(repo_path, RecursiveMode::Recursive).unwrap(); + watcher.watch(source_dir, RecursiveMode::Recursive).unwrap(); // Start event loop + println!("Starting event loop..."); let mut app = App::default(); - let _ = event_loop.run_app(&mut app); + event_loop.run_app(&mut app).unwrap(); }