Implement file syncing to a seperate folder with the git repository
This commit is contained in:
parent
763624a226
commit
467d86f53d
|
|
@ -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
97
src/filesync.rs
Normal 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
8
src/gitpush.rs
Normal 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());
|
||||
}
|
||||
51
src/main.rs
51
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<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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user