symlink support

This commit is contained in:
Robin Appelman 2025-10-13 18:30:48 +02:00
commit 1269e95b6f
2 changed files with 41 additions and 9 deletions

View file

@ -8,6 +8,8 @@ use thiserror::Error;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct GaltonConfig { pub struct GaltonConfig {
#[serde(default)]
pub watch: WatchConfig,
#[serde(default)] #[serde(default)]
pub rule: Vec<Rule>, pub rule: Vec<Rule>,
} }
@ -26,15 +28,26 @@ impl GaltonConfig {
} }
} }
fn normalize_path(path: String) -> String { #[derive(Debug, Deserialize, Default)]
if let Some(suffix) = path.strip_prefix("~/") { pub struct WatchConfig {
symlink: Option<String>,
}
impl WatchConfig {
pub fn symlink(&self) -> Option<String> {
self.symlink.as_deref().map(normalize_path)
}
}
fn normalize_path<P: Into<String> + AsRef<str>>(path: P) -> String {
if let Some(suffix) = path.as_ref().strip_prefix("~/") {
let home = home_dir().unwrap_or_default(); let home = home_dir().unwrap_or_default();
home.join(suffix) home.join(suffix)
.into_os_string() .into_os_string()
.into_string() .into_string()
.expect("non utf8 home directory") .expect("non utf8 home directory")
} else { } else {
path path.into()
} }
} }

View file

@ -10,6 +10,7 @@ use notify_debouncer_full::notify::{EventKind, RecursiveMode};
use notify_debouncer_full::{new_debouncer, DebounceEventResult}; use notify_debouncer_full::{new_debouncer, DebounceEventResult};
use std::fs::{copy, create_dir_all, remove_file, rename}; use std::fs::{copy, create_dir_all, remove_file, rename};
use std::io::ErrorKind; use std::io::ErrorKind;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::thread::sleep; use std::thread::sleep;
@ -66,6 +67,7 @@ fn main() -> MainResult {
} }
Commands::Watch { path, recursive } => { Commands::Watch { path, recursive } => {
let rules = config.rule; let rules = config.rule;
let symlink = config.watch.symlink();
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher = new_debouncer(Duration::from_secs(1), None, tx)?; let mut watcher = new_debouncer(Duration::from_secs(1), None, tx)?;
watcher.watch( watcher.watch(
@ -84,7 +86,7 @@ fn main() -> MainResult {
} }
})?; })?;
for res in rx { for res in rx {
handle_watch_events(res, &rules); handle_watch_event(res, &rules, symlink.as_deref());
} }
} }
} }
@ -92,7 +94,7 @@ fn main() -> MainResult {
Ok(()) Ok(())
} }
fn handle_watch_events(result: DebounceEventResult, rules: &[Rule]) { fn handle_watch_event(result: DebounceEventResult, rules: &[Rule], link_target: Option<&str>) {
match result { match result {
Ok(events) => { Ok(events) => {
for event in events { for event in events {
@ -103,7 +105,22 @@ fn handle_watch_events(result: DebounceEventResult, rules: &[Rule]) {
sleep(Duration::from_millis(200)); sleep(Duration::from_millis(200));
match FileInfo::load(path) { match FileInfo::load(path) {
Ok(file) => { Ok(file) => {
handle_file(&file, rules); if let Some(target) = handle_file(&file, rules) {
if let Some(link_target) = link_target {
match symlink(&target, link_target) {
Ok(()) => {
info!(
to = target,
from = link_target,
"created symlink"
);
}
Err(error) => {
error!(%error, "failed to link target");
}
}
}
}
} }
Err(error) => { Err(error) => {
error!(%error, "failed to load file info"); error!(%error, "failed to load file info");
@ -132,10 +149,10 @@ fn match_file(file: &FileInfo, rules: &[Rule]) -> Option<RuleMatch> {
} }
#[instrument(skip_all, fields(file = file.path))] #[instrument(skip_all, fields(file = file.path))]
fn handle_file(file: &FileInfo, rules: &[Rule]) { fn handle_file(file: &FileInfo, rules: &[Rule]) -> Option<String> {
let Some(result) = match_file(file, rules) else { let Some(result) = match_file(file, rules) else {
info!("no matches"); info!("no matches");
return; return None;
}; };
let parent = result.target.as_deref().unwrap_or_else(|| file.parent()); let parent = result.target.as_deref().unwrap_or_else(|| file.parent());
@ -143,7 +160,7 @@ fn handle_file(file: &FileInfo, rules: &[Rule]) {
if let Err(error) = create_dir_all(parent) { if let Err(error) = create_dir_all(parent) {
error!(%error, "failed to create target directory"); error!(%error, "failed to create target directory");
return; return None;
} }
let target = format!("{parent}/{name}"); let target = format!("{parent}/{name}");
@ -151,9 +168,11 @@ fn handle_file(file: &FileInfo, rules: &[Rule]) {
match cross_storage_move(&file.path, &target) { match cross_storage_move(&file.path, &target) {
Ok(()) => { Ok(()) => {
info!(target, "moved file"); info!(target, "moved file");
Some(target)
} }
Err(error) => { Err(error) => {
info!(target, ?error, "failed to moved file"); info!(target, ?error, "failed to moved file");
None
} }
} }
} }