Implement file syncing to a seperate folder with the git repository

This commit is contained in:
Robin Gottschalk 2025-02-02 10:59:45 +01:00
parent 763624a226
commit 467d86f53d
4 changed files with 148 additions and 16 deletions

View File

@ -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)

97
src/filesync.rs Normal file
View File

@ -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<dyn std::error::Error>> {
// Ensure the target directory exists
fs::create_dir_all(target)?;
// Gather all paths in the source and target directories
let filter = |entry: Result<fs::DirEntry, std::io::Error>| {
entry.ok().and_then(|entry| {
if entry.file_name().to_str() != protect {
Some(entry.path())
} else {
None
}
})
};
let mut source_entries: Vec<PathBuf> = fs::read_dir(source)?.filter_map(filter).collect();
let mut target_entries: Vec<PathBuf> = 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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
println!("File/dir removed: {:?}", path);
if path.is_file() {
fs::remove_file(path)?;
} else if path.is_dir() {
fs::remove_dir_all(path)?;
}
Ok(())
}

8
src/gitpush.rs Normal file
View File

@ -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());
}

View File

@ -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<UserEvent> 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<UserEvent> 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<UserEvent> 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<UserEvent> 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<UserEvent> 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::<UserEvent>::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();
}