obsidian-git-backup/src/main.rs
2025-03-14 09:33:55 +01:00

183 lines
6.3 KiB
Rust

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use notify::{Event, RecursiveMode, Watcher};
use std::path::Path;
use std::time::{Duration, Instant};
use tray_icon::{
menu::{Menu, MenuItem},
Icon, TrayIconBuilder,
};
use winit::event::StartCause;
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
};
mod filesync;
mod git;
const PROTECT_PREFIX: &str = ".git"; // only at first directory level
const SOURCE_VAULT: &str = "D:\\Cloud\\Syncthing\\obsidian-vault"; // syncthing vault folder
const REPO_VAULT: &str =
"C:\\Users\\robin\\AppData\\Roaming\\obsidian-git-backup\\obsidian-vault-clone"; // git repo folder
const GIT_COMMIT_DELAY_AFTER_FILE_CHANGE: Duration = Duration::from_secs(30 * 60); // 30 min
#[derive(Debug)]
enum UserEvent {
MenuEvent(tray_icon::menu::MenuEvent),
FileEvent(Event),
}
#[derive(Default)]
struct App {
tray_icon: Option<tray_icon::TrayIcon>,
menu_changed_status: Option<MenuItem>,
menu_backup_now: Option<MenuItem>,
menu_exit: Option<MenuItem>,
last_change_time: Option<Instant>,
current_change_count: usize,
}
impl App {
fn create_tray_icon_menu(&mut self) {
// Create and show the tray icon
let menu_item_changed_status = MenuItem::new("Changed: 0", true, None);
let menu_item_backup_now = MenuItem::new("Backup Now", true, None);
let menu_item_exit = MenuItem::new("Exit", true, None);
let tray_menu = Menu::new();
tray_menu.append(&menu_item_changed_status).unwrap();
tray_menu.append(&menu_item_backup_now).unwrap();
tray_menu.append(&menu_item_exit).unwrap();
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))
.with_tooltip("Obsidian Git Backup")
.with_icon(icon)
.build()
.unwrap();
self.tray_icon = Some(tray_icon);
self.menu_changed_status = Some(menu_item_changed_status);
self.menu_backup_now = Some(menu_item_backup_now);
self.menu_exit = Some(menu_item_exit);
}
fn start_backup(&mut self) {
self.last_change_time = None;
let repo_dir = Path::new(REPO_VAULT);
git::backup_changes(repo_dir).expect("Changes could not be pushed");
self.current_change_count = 0;
self.menu_update_change_count();
}
fn process_file_event(&mut self) {
self.last_change_time = Some(Instant::now());
let source = Path::new(SOURCE_VAULT);
let target = Path::new(REPO_VAULT);
filesync::sync_directory(source, target, Some(PROTECT_PREFIX))
.expect("Directories could not be synced");
self.current_change_count =
git::current_change_count(target).expect("Changes could not get counted using git");
self.menu_update_change_count();
println!("Currently changed files: {:?}", self.current_change_count);
}
fn menu_update_change_count(&mut self) {
if let Some(menu_item) = &mut self.menu_changed_status {
menu_item.set_text(format!("Changed: {:?}", self.current_change_count));
}
}
}
impl ApplicationHandler<UserEvent> for App {
fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
self.create_tray_icon_menu();
self.last_change_time = None;
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::MenuEvent(menu_event) => {
match menu_event.id() {
id if id == self.menu_backup_now.as_ref().unwrap().id() => {
println!("---> Backup triggered by menu");
// Handle push now menu item
self.start_backup();
}
id if id == self.menu_exit.as_ref().unwrap().id() => {
println!("---> Exit triggert by menu");
// Handle exit menu item
event_loop.exit();
}
_ => {}
}
}
UserEvent::FileEvent(_event) => {
println!("---> File change detected");
self.process_file_event();
}
}
}
fn new_events(&mut self, event_loop: &ActiveEventLoop, _cause: StartCause) {
let now = Instant::now();
// 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) > GIT_COMMIT_DELAY_AFTER_FILE_CHANGE {
println!("---> Backup triggered by file change");
self.start_backup();
}
}
// Reset timer
event_loop.set_control_flow(ControlFlow::WaitUntil(now + Duration::from_secs(5)));
}
fn window_event(
&mut self,
_event_loop: &ActiveEventLoop,
_window_id: winit::window::WindowId,
_event: WindowEvent,
) {
}
}
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);
filesync::sync_directory(source_dir, repo_dir, Some(PROTECT_PREFIX))
.expect("Directories could not be synced");
git::backup_changes(repo_dir).expect("Changes could not be pushed");
// 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| {
proxy.send_event(UserEvent::MenuEvent(event)).unwrap();
}));
// Set up file change event handler
let proxy = event_loop.create_proxy();
let mut watcher = notify::recommended_watcher(move |event| {
if let Ok(event) = event {
proxy.send_event(UserEvent::FileEvent(event)).unwrap();
}
})
.unwrap();
watcher.watch(source_dir, RecursiveMode::Recursive).unwrap();
// Start event loop
println!("Starting event loop...");
let mut app = App::default();
event_loop.run_app(&mut app).unwrap();
}