diff --git a/Cargo.lock b/Cargo.lock index f870e0682..423c46960 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -851,6 +851,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.31" @@ -1349,6 +1358,26 @@ dependencies = [ "hashbrown 0.15.3", ] +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.13" @@ -1464,6 +1493,26 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1593,6 +1642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1641,6 +1691,31 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "notify" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" +dependencies = [ + "bitflags 2.9.0", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.59.0", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "num-bigint" version = "0.4.6" @@ -2363,6 +2438,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.27" @@ -2940,6 +3024,7 @@ dependencies = [ "libc", "log", "mime", + "notify", "openssl", "paste", "path-clean", @@ -3091,6 +3176,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3253,6 +3348,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 55aa7f194..5b84e2dad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ base64 = "0" paste = "1" bitflags = "2" indexmap = "2" +notify = "8" #[cfg(target_os = "macos")] libc = "0" #[cfg(target_os = "windows")] diff --git a/src/api/config_watch.rs b/src/api/config_watch.rs new file mode 100644 index 000000000..eda533e9f --- /dev/null +++ b/src/api/config_watch.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Result, Watcher}; +use std::sync::{mpsc, Arc}; +use log::info; +use notify::event::{AccessKind, AccessMode}; +use crate::api::model::app_state::AppState; + +pub async fn exec_config_watch(app_state: &Arc) -> notify::Result<()> { + let (tx, rx) = mpsc::channel::>(); + + let mut files = HashMap::new(); + files.insert(PathBuf::from(&app_state.config.t_config_file_path), "config"); + files.insert(PathBuf::from(&app_state.config.t_api_proxy_file_path),"api-proxy"); + files.insert(PathBuf::from(&app_state.config.t_mapping_file_path),"mapping"); + files.insert(PathBuf::from(&app_state.config.t_sources_file_path),"sources"); + + // Use recommended_watcher() to automatically select the best implementation + // for your platform. The `EventHandler` passed to this constructor can be a + // closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or + // another type the trait is implemented for. + let mut watcher = recommended_watcher(tx)?; + + // Add a path to be watched. All files and directories at that path and + // below will be monitored for changes. + let path = Path::new(app_state.config.t_config_path.as_str()); + info!("Watching changes {path:?}"); + watcher.watch(path, RecursiveMode::NonRecursive)?; + + tokio::spawn(async move { + let _keep_watcher_alive = watcher; + for res in rx { + match res { + Ok(event) => { + if let EventKind::Access(AccessKind::Close(AccessMode::Write)) = event.kind { + for path in event.paths { + if let Some(file) = files.get(&path) { + println!("File changed {file}: {path:?}"); + } + } + } + } + Err(e) => println!("watch error: {e:?}"), + } + } + info!("Watching stopped"); + }); + + Ok(()) +} \ No newline at end of file diff --git a/src/api/main_api.rs b/src/api/main_api.rs index 97035ae87..973e2f897 100644 --- a/src/api/main_api.rs +++ b/src/api/main_api.rs @@ -27,6 +27,7 @@ use axum::Router; use tokio::sync::Mutex; use tower_governor::key_extractor::SmartIpKeyExtractor; use crate::api::api_utils::{get_build_time, get_server_time}; +use crate::api::config_watch::exec_config_watch; use crate::utils::request::create_client; use crate::VERSION; @@ -230,6 +231,11 @@ pub async fn start_server(cfg: Arc, targets: Arc) -> fut exec_scheduler(&Arc::clone(&shared_data.http_client), &cfg, &targets); exec_update_on_boot(Arc::clone(&shared_data.http_client), &cfg, &targets); + + if let Err(err) = exec_config_watch(&app_state).await { + error!("Failed to start config watch: {err}"); + } + let web_auth_enabled = is_web_auth_enabled(&cfg, web_ui_enabled); if cfg.t_api_proxy.read().await.is_some() { @@ -288,6 +294,7 @@ pub async fn start_server(cfg: Arc, targets: Arc) -> fut } } + fn add_rate_limiter(router: Router>, rate_limit_cfg: &RateLimitConfig) -> Router> { if rate_limit_cfg.enabled { let governor_conf = Arc::new(tower_governor::governor::GovernorConfigBuilder::default() diff --git a/src/api/mod.rs b/src/api/mod.rs index e25bb6c0f..2b3a485a8 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,3 +3,4 @@ pub mod api_utils; mod scheduler; mod endpoints; pub mod main_api; +mod config_watch; diff --git a/src/foundation/filter.rs b/src/foundation/filter.rs index 4640a8aa2..fcafe0cbb 100644 --- a/src/foundation/filter.rs +++ b/src/foundation/filter.rs @@ -555,8 +555,7 @@ pub fn prepare_templates( pub fn apply_templates_to_pattern(pattern: &str, templates: &Vec) -> String { let mut new_pattern = pattern.to_string(); for template in templates { - new_pattern = - new_pattern.replace(format!("!{}!", &template.name).as_str(), &template.value); + new_pattern = new_pattern.replace(format!("!{}!", &template.name).as_str(), &template.value); } new_pattern } diff --git a/src/main.rs b/src/main.rs index 7bd4e1715..503fab50f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -100,7 +100,7 @@ fn main() { return; } - let config_path: String = args.config_path.unwrap_or_else(file_utils::get_default_config_path); + let config_path: String = file_utils::resolve_directory_path(&args.config_path.unwrap_or_else(file_utils::get_default_config_path)); let config_file: String = args.config_file.unwrap_or_else(|| file_utils::get_default_config_file_path(&config_path)); let env_log_level = std::env::var("TULIPROX_LOG"); @@ -125,11 +125,21 @@ fn main() { if let Some(bts) = BUILD_TIMESTAMP.to_string().parse::>().ok().map(|datetime| datetime.format("%Y-%m-%d %H:%M:%S %Z").to_string()) { info!("Build time: {bts}"); } + + match config_reader::read_mappings(args.mapping_file, &mut cfg, true) { + Ok(Some(mapping_file)) => { + cfg.t_mapping_file_path = mapping_file; + } + Ok(None) => {}, + Err(err) => exit!("{err}"), + } + info!("Current time: {}", chrono::offset::Local::now().format("%Y-%m-%d %H:%M:%S")); info!("Working dir: {:?}", &cfg.working_dir); info!("Config dir: {:?}", &cfg.t_config_path); info!("Config file: {config_file:?}"); info!("Source file: {sources_file:?}"); + info!("Mapping file: {:?}", cfg.t_mapping_file_path); info!("Temp dir: {temp_path:?}"); if let Some(cache) = cfg.reverse_proxy.as_ref().and_then(|r| r.cache.as_ref()) { if cache.enabled { @@ -137,14 +147,6 @@ fn main() { } } - match config_reader::read_mappings(args.mapping_file, &mut cfg, true) { - Ok(Some(mapping_file)) => { - info!("Mapping file: {mapping_file:?}"); - } - Ok(None) => {} - Err(err) => exit!("{err}"), - } - // if cfg.t_channel_unavailable_video.is_some() { // info!("Channel unavailable video loaded from {:?}", cfg.channel_unavailable_file.as_ref().map_or("?", |v| v.as_str())); // } diff --git a/src/model/config.rs b/src/model/config.rs index 21780a534..816c82599 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -382,6 +382,8 @@ pub struct Config { #[serde(skip)] pub t_sources_file_path: String, #[serde(skip)] + pub t_mapping_file_path: String, + #[serde(skip)] pub t_api_proxy_file_path: String, #[serde(skip)] pub file_locks: Arc, @@ -636,7 +638,7 @@ impl Config { */ pub fn prepare(&mut self, include_computed: bool) -> Result<(), TuliProxError> { let work_dir = &self.working_dir; - self.working_dir = file_utils::get_working_path(work_dir); + self.working_dir = file_utils::resolve_directory_path(work_dir); if include_computed { self.t_access_token_secret = generate_secret(); self.t_encrypt_secret = <&[u8] as TryInto<[u8; 16]>>::try_into(&generate_secret()[0..16]).map_err(|err| TuliProxError::new(TuliProxErrorKind::Info, err.to_string()))?; diff --git a/src/model/config_rename.rs b/src/model/config_rename.rs index 04f296f1a..92b3e195d 100644 --- a/src/model/config_rename.rs +++ b/src/model/config_rename.rs @@ -1,3 +1,4 @@ +use crate::foundation::filter::{apply_templates_to_pattern, PatternTemplate}; use crate::tuliprox_error::{TuliProxError, TuliProxErrorKind, create_tuliprox_error_result}; use crate::model::ItemField; @@ -12,7 +13,10 @@ pub struct ConfigRename { } impl ConfigRename { - pub fn prepare(&mut self) -> Result<(), TuliProxError> { + pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliProxError> { + if let Some(templ) = templates { + self.pattern = apply_templates_to_pattern(&self.pattern, templ); + } match regex::Regex::new(&self.pattern) { Ok(pattern) => { self.re = Some(pattern); diff --git a/src/model/config_sort.rs b/src/model/config_sort.rs index deee771db..9d87aa79d 100644 --- a/src/model/config_sort.rs +++ b/src/model/config_sort.rs @@ -1,4 +1,5 @@ use regex::Regex; +use crate::foundation::filter::{apply_templates_to_pattern, PatternTemplate}; use crate::tuliprox_error::{TuliProxError, TuliProxErrorKind, create_tuliprox_error, handle_tuliprox_error_result_list}; use crate::model::{ItemField}; @@ -29,14 +30,21 @@ pub struct ConfigSortGroup { #[serde(default, skip_serializing_if = "Option::is_none")] pub sequence: Option>, #[serde(default, skip)] - pub t_sequence: Option>, + pub t_re_sequence: Option>, } impl ConfigSortGroup { - pub fn prepare(&mut self) -> Result<(), TuliProxError> { - // Compile sequence patterns, if any - self.t_sequence = compile_regex_vec(self.sequence.as_ref())?; + pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliProxError> { + // Transform sequence with templates if provided, otherwise use raw sequence + let processed_sequence = match (&self.sequence, templates) { + (Some(seqs), Some(tmpls)) => Some(seqs.iter().map(|s| apply_templates_to_pattern(s, tmpls)).collect()), + (Some(seqs), None) => Some(seqs.clone()), + (None, _) => None, + }; + + // Compile regex patterns + self.t_re_sequence = compile_regex_vec(processed_sequence.as_ref())?; Ok(()) } } @@ -52,13 +60,16 @@ pub struct ConfigSortChannel { #[serde(default, skip_serializing_if = "Option::is_none")] pub sequence: Option>, #[serde(default, skip)] - pub t_sequence: Option>, + pub t_re_sequence: Option>, #[serde(skip)] pub t_re_group_pattern: Option, } impl ConfigSortChannel { - pub fn prepare(&mut self) -> Result<(), TuliProxError> { + pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliProxError> { + if let Some(templ) = templates { + self.group_pattern = apply_templates_to_pattern(&self.group_pattern, templ); + } // Compile group_pattern self.t_re_group_pattern = Some( Regex::new(&self.group_pattern).map_err(|err| { @@ -66,8 +77,15 @@ impl ConfigSortChannel { })? ); - // Compile sequence patterns, if any - self.t_sequence = compile_regex_vec(self.sequence.as_ref())?; + // Transform sequence with templates if provided, otherwise use raw sequence + let processed_sequence = match (&self.sequence, templates) { + (Some(seqs), Some(tmpls)) => Some(seqs.iter().map(|s| apply_templates_to_pattern(s, tmpls)).collect()), + (Some(seqs), None) => Some(seqs.clone()), + (None, _) => None, + }; + + // Compile regex patterns + self.t_re_sequence = compile_regex_vec(processed_sequence.as_ref())?; Ok(()) } } @@ -84,12 +102,12 @@ pub struct ConfigSort { } impl ConfigSort { - pub fn prepare(&mut self) -> Result<(), TuliProxError> { + pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliProxError> { if let Some(group) = self.groups.as_mut() { - group.prepare()?; + group.prepare(templates)?; } if let Some(channels) = self.channels.as_mut() { - handle_tuliprox_error_result_list!(TuliProxErrorKind::Info, channels.iter_mut().map(ConfigSortChannel::prepare)); + handle_tuliprox_error_result_list!(TuliProxErrorKind::Info, channels.iter_mut().map(|csc| csc.prepare(templates))); } Ok(()) } diff --git a/src/model/config_target.rs b/src/model/config_target.rs index 40b57288c..30a298b4d 100644 --- a/src/model/config_target.rs +++ b/src/model/config_target.rs @@ -288,10 +288,10 @@ impl ConfigTarget { // debug!("Filter: {}", fltr); self.t_filter = Some(fltr); if let Some(renames) = self.rename.as_mut() { - handle_tuliprox_error_result_list!(TuliProxErrorKind::Info, renames.iter_mut().map(ConfigRename::prepare)); + handle_tuliprox_error_result_list!(TuliProxErrorKind::Info, renames.iter_mut().map(|cr|cr.prepare(templates))); } if let Some(sort) = self.sort.as_mut() { - handle_tuliprox_error_result!(TuliProxErrorKind::Info, sort.prepare()); + handle_tuliprox_error_result!(TuliProxErrorKind::Info, sort.prepare(templates)); } Ok(()) } diff --git a/src/model/mapping.rs b/src/model/mapping.rs index 9b13abf51..7c2170c11 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -219,11 +219,19 @@ impl Mapper { /// /// Will panic if default `RegEx` gets invalid pub fn prepare(&mut self, templates: Option<&Vec>, tags: Option<&Vec>) -> Result<(), TuliProxError> { - for key in self.attributes.keys() { + for (key, value) in &self.attributes.clone() { if !valid_property!(key.as_str(), MAPPER_ATTRIBUTE_FIELDS) { return Err(info_err!(format!("Invalid mapper attribute field {key}"))); } + + if let Some(template_list) = templates { + let new_value = apply_templates_to_pattern(value, template_list); + if new_value != *value { + self.attributes.insert(key.clone(), new_value); + } + } } + for key in self.suffix.keys() { if !valid_property!(key.as_str(), AFFIX_FIELDS) { return Err(info_err!(format!("Invalid mapper suffix field {key}"))); diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index d94e979b3..f7867eb72 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -85,7 +85,7 @@ fn exec_rename(pli: &mut PlaylistItem, rename: Option<&Vec>) { for r in renames { let value = get_field_value(result, &r.field); let cap = r.re.as_ref().unwrap().replace_all(value.as_str(), &r.new_name); - if log::log_enabled!(log::Level::Debug) && *value != cap { + if log_enabled!(log::Level::Debug) && *value != cap { debug_if_enabled!("Renamed {}={} to {}", &r.field, value, cap); } let value = cap.into_owned(); @@ -547,7 +547,7 @@ pub async fn exec_processing(client: Arc, cfg: Arc, tar } } let elapsed = start_time.elapsed().as_secs(); - info!("Update process finished! Took {elapsed} secs."); + info!("🌷 Update process finished! Took {elapsed} secs."); } #[cfg(test)] diff --git a/src/processing/processor/sort.rs b/src/processing/processor/sort.rs index 3ae43ad84..9a7e6e7b9 100644 --- a/src/processing/processor/sort.rs +++ b/src/processing/processor/sort.rs @@ -98,7 +98,7 @@ fn playlistgroup_comparator(a: &PlaylistGroup, b: &PlaylistGroup, group_sort: &C let value_a = if match_as_ascii { deunicode(&a.title) } else { a.title.to_string() }; let value_b = if match_as_ascii { deunicode(&b.title) } else { b.title.to_string() }; - playlist_comparator(group_sort.t_sequence.as_ref(), &group_sort.order, &value_a, &value_b) + playlist_comparator(group_sort.t_re_sequence.as_ref(), &group_sort.order, &value_a, &value_b) } fn playlistitem_comparator( @@ -112,7 +112,7 @@ fn playlistitem_comparator( let value_a = if match_as_ascii { deunicode(&raw_value_a) } else { raw_value_a }; let value_b = if match_as_ascii { deunicode(&raw_value_b) } else { raw_value_b }; - playlist_comparator(channel_sort.t_sequence.as_ref(), &channel_sort.order, &value_a, &value_b) + playlist_comparator(channel_sort.t_re_sequence.as_ref(), &channel_sort.order, &value_a, &value_b) } pub(in crate::processing::processor) fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) { @@ -154,7 +154,7 @@ mod tests { group_pattern: ".*".to_string(), order: SortOrder::Asc, sequence: None, - t_sequence: Some(vec![ + t_re_sequence: Some(vec![ Regex::new(r"(?P.*?)\bUHD\b").unwrap(), Regex::new(r"(?P.*?)\bFHD\b").unwrap(), Regex::new(r"(?P.*?)\bHD\b").unwrap(), @@ -202,7 +202,7 @@ mod tests { group_pattern: ".*US.*".to_string(), order: SortOrder::Asc, sequence: None, - t_sequence: Some(vec![ + t_re_sequence: Some(vec![ Regex::new(r"^US\| EAST.*?\[\bUHD\b\](?P.*)").unwrap(), Regex::new(r"^US\| EAST.*?\[\bFHD\b\](?P.*)").unwrap(), Regex::new(r"^US\| EAST.*?\[\bHD\b\](?P.*)").unwrap(), diff --git a/src/utils/file/config_reader.rs b/src/utils/file/config_reader.rs index 3a4109315..a85529b0d 100644 --- a/src/utils/file/config_reader.rs +++ b/src/utils/file/config_reader.rs @@ -53,7 +53,7 @@ pub fn read_mappings(args_mapping: Option, cfg: &mut Config, resolve_env } Some(mappings_cfg) => { cfg.set_mappings(&mappings_cfg); - Ok(None) + Ok(Some(mappings_file)) } } } diff --git a/src/utils/file/file_utils.rs b/src/utils/file/file_utils.rs index 99c71e24f..4e9574a43 100644 --- a/src/utils/file/file_utils.rs +++ b/src/utils/file/file_utils.rs @@ -88,27 +88,39 @@ pub fn get_default_api_proxy_config_path(config_path: &str) -> String { get_default_file_path(config_path, API_PROXY_FILE) } -pub fn get_working_path(wd: &str) -> String { +pub fn resolve_directory_path(input: &str) -> String { let current_dir = std::env::current_dir().unwrap_or_default(); - if wd.is_empty() { - String::from(current_dir.to_str().unwrap_or(".")) - } else { - let work_path = std::path::PathBuf::from(wd); - let _ = fs::create_dir_all(&work_path); - let wdpath = fs::metadata(&work_path).map_or(None, |md| if md.is_dir() && !md.permissions().readonly() { - work_path.canonicalize().ok() - } else { - error!("Path not found {:?}", &work_path); - None - }); - let rp: PathBuf = wdpath.map_or_else(|| current_dir.join(wd), |d| d); - rp.canonicalize().map_or_else(|_| { - error!("Path not found {:?}", &rp); - String::from("./") - }, |ap| String::from(ap.to_str().unwrap_or("./"))) + + if input.is_empty() { + return String::from(current_dir.to_str().unwrap_or(".")); } + + let input_path = PathBuf::from(input); + let _ = fs::create_dir_all(&input_path); + + let resolved_path = fs::metadata(&input_path).ok().and_then(|md| { + if md.is_dir() && !md.permissions().readonly() { + input_path.canonicalize().ok() + } else { + error!("Path not found or not writable: {:?}", &input_path); + None + } + }); + + let final_path = resolved_path.unwrap_or_else(|| current_dir.join(input)); + + final_path + .canonicalize() + .map_or_else( + |_| { + error!("Path not found {:?}", &final_path); + String::from("./") + }, + |ap| String::from(ap.to_str().unwrap_or("./")), + ) } + #[inline] pub fn open_file(file_name: &Path) -> Result { File::open(file_name)