diff --git a/.gitignore b/.gitignore index a779e9d..b37c1eb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ *.tar.gz /test-files/LIB_* !/test-files/LIB_*.zip +git.txt diff --git a/ll-cli/src/main.rs b/ll-cli/src/main.rs index 5c95c2c..70c85fd 100644 --- a/ll-cli/src/main.rs +++ b/ll-cli/src/main.rs @@ -8,7 +8,7 @@ fn main() -> ll_core::Result<()> { let app = clap::App::from(app_yaml) .about(env!("CARGO_PKG_DESCRIPTION")) .author(env!("CARGO_PKG_AUTHORS")) - .version(env!("CARGO_PKG_VERSION")) + .version(format!("{} ({})", env!("CARGO_PKG_VERSION"), ll_core::GIT_DESCRIBE).as_str()) .get_matches(); #[cfg(not(debug_assertions))] @@ -32,7 +32,7 @@ fn main() -> ll_core::Result<()> { Config::default_path() } else { app.value_of("config") - .map(|p| PathBuf::from(p)) + .map(PathBuf::from) .or(Config::get_path()?) } { Some(path) => path, @@ -59,7 +59,7 @@ fn main() -> ll_core::Result<()> { } println!("Using config at {:?}", config_path); - let mut config = Config::read(Some(config_path.clone()))?; + let mut config = Config::read(Some(config_path))?; if let Some(watch_path) = app.value_of("watch") { config.settings.watch_path = watch_path.into(); diff --git a/ll-core/Cargo.toml b/ll-core/Cargo.toml index 4e974af..1db21b0 100644 --- a/ll-core/Cargo.toml +++ b/ll-core/Cargo.toml @@ -4,6 +4,7 @@ version = "0.3.1" authors = ["Edwin Svensson "] edition = "2021" publish = false +build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/ll-core/build.rs b/ll-core/build.rs new file mode 100644 index 0000000..a9fab59 --- /dev/null +++ b/ll-core/build.rs @@ -0,0 +1,11 @@ +use std::{fs, process::Command}; + +fn main() { + // Git version + let git_desc = Command::new("git") + .args(&["describe", "--all", "--tags", "--dirty", "--long"]) + .output() + .unwrap(); + + fs::write("git.txt", String::from_utf8_lossy(&git_desc.stdout).trim()).unwrap(); +} diff --git a/ll-core/src/config/mod.rs b/ll-core/src/config/mod.rs index ccaed0f..775cabf 100644 --- a/ll-core/src/config/mod.rs +++ b/ll-core/src/config/mod.rs @@ -21,6 +21,8 @@ pub struct Format { pub struct Settings { pub watch_path: String, pub recursive: bool, + #[serde(default = "default_ignore_temp")] + pub ignore_temp: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -42,7 +44,7 @@ impl Config { } pub fn save(&self, path: Option) -> Result<()> { - let p = Self::path(path.or(self._self_path.clone()))?; + let p = Self::path(path.or_else(|| self._self_path.clone()))?; let toml_str = toml::to_string_pretty(self)?; fs::write(p, toml_str)?; Ok(()) @@ -50,7 +52,7 @@ impl Config { pub(crate) fn formats(&self) -> Result> { let mut formats_vec = Vec::with_capacity(self.formats.len()); - for (_, f) in &self.formats { + for f in self.formats.values() { formats_vec.push(format::Format::from_ecad( f.format.clone(), PathBuf::from(shellexpand::full(&f.output_path)?.as_ref()), @@ -60,8 +62,8 @@ impl Config { } fn path(path: Option) -> Result { - path.or(Self::default_path()) - .ok_or(Error::Other("Could not find config dir".into())) + path.or_else(Self::default_path) + .ok_or(Error::Other("Could not find config dir")) } pub fn default_path() -> Option { @@ -94,6 +96,7 @@ impl Default for Config { .to_string_lossy() .to_string(), recursive: false, + ignore_temp: default_ignore_temp(), }, formats: HashMap::new(), profile: Profile { @@ -103,3 +106,7 @@ impl Default for Config { } } } + +const fn default_ignore_temp() -> bool { + true +} diff --git a/ll-core/src/cse/mod.rs b/ll-core/src/cse/mod.rs index 1ec179d..5257c75 100644 --- a/ll-core/src/cse/mod.rs +++ b/ll-core/src/cse/mod.rs @@ -12,6 +12,7 @@ use { mod result; pub use result::Result; +#[allow(clippy::upper_case_acronyms)] pub struct CSE { auth: String, formats: Arc>, diff --git a/ll-core/src/cse/result.rs b/ll-core/src/cse/result.rs index cabf74b..a199e82 100644 --- a/ll-core/src/cse/result.rs +++ b/ll-core/src/cse/result.rs @@ -16,7 +16,7 @@ impl Result { pub fn save(&self) -> error::Result { let save_dir = Path::new(&self.output_path); - if &self.files.len() > &0 { + if !self.files.is_empty() { if !save_dir.exists() { fs::create_dir_all(save_dir)?; } diff --git a/ll-core/src/epw.rs b/ll-core/src/epw.rs index 0339449..4ba38bf 100644 --- a/ll-core/src/epw.rs +++ b/ll-core/src/epw.rs @@ -43,7 +43,7 @@ impl Epw { }; for line in lines { - let line_parts: Vec<&str> = line.split("=").collect(); + let line_parts: Vec<&str> = line.split('=').collect(); if line_parts.len() == 2 { map.insert(line_parts[0], line_parts[1]); @@ -51,7 +51,7 @@ impl Epw { } Ok(Self { - id: id, + id, mna: String::from(*map.get("mna").unwrap_or(&"")), mpn: String::from(*map.get("mpn").unwrap_or(&"")), pna: String::from(*map.get("pna").unwrap_or(&"")), @@ -82,7 +82,7 @@ impl Epw { fn from_zip(raw_data: Vec) -> Result { // The zip library crashes if the archive is empty, // lets prevent that. - if raw_data.len() == 0 { + if raw_data.is_empty() { return Err(Error::ZipArchiveEmpty); } diff --git a/ll-core/src/format/mod.rs b/ll-core/src/format/mod.rs index 403f5e0..a0d93ab 100644 --- a/ll-core/src/format/mod.rs +++ b/ll-core/src/format/mod.rs @@ -119,11 +119,11 @@ impl Format { pub fn extract(&self, files: &mut Files, file_path: String, item: &mut ZipFile) -> Result<()> { match &self.ecad { // * Keep these in alphabetical order - ECAD::D3 => extractors::d3::extract(&self, files, file_path, item)?, - ECAD::DesignSpark => extractors::designspark::extract(&self, files, file_path, item)?, - ECAD::Eagle => extractors::eagle::extract(&self, files, file_path, item)?, - ECAD::EasyEDA => extractors::easyeda::extract(&self, files, file_path, item)?, - ECAD::KiCad => extractors::kicad::extract(&self, files, file_path, item)?, + ECAD::D3 => extractors::d3::extract(self, files, file_path, item)?, + ECAD::DesignSpark => extractors::designspark::extract(self, files, file_path, item)?, + ECAD::Eagle => extractors::eagle::extract(self, files, file_path, item)?, + ECAD::EasyEDA => extractors::easyeda::extract(self, files, file_path, item)?, + ECAD::KiCad => extractors::kicad::extract(self, files, file_path, item)?, ECAD::Zip => unreachable!("ZIP not handled!"), // ! NOTE: DO NOT ADD A _ => {} CATCHER HERE! }; diff --git a/ll-core/src/lib.rs b/ll-core/src/lib.rs index 611cc28..5cf1a2c 100644 --- a/ll-core/src/lib.rs +++ b/ll-core/src/lib.rs @@ -9,6 +9,9 @@ mod updates; mod utils; mod watcher; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const GIT_DESCRIBE: &str = include_str!("../git.txt"); + pub use { config::{profile::Profile, Config, Format}, consts::LL_CONFIG, diff --git a/ll-core/src/logger/mod.rs b/ll-core/src/logger/mod.rs index a8f4499..796752a 100644 --- a/ll-core/src/logger/mod.rs +++ b/ll-core/src/logger/mod.rs @@ -11,36 +11,37 @@ pub trait Logger: Send + Sync { #[macro_export] macro_rules! log_trace { - ($loggers:expr, $msg:expr) => { + ($loggers:expr, $($arg:tt)*) => { + #[cfg(debug_assertions)] for l in $loggers { - l.trace(format!("{}", $msg)) + l.trace(format!($($arg)*)) } }; } #[macro_export] macro_rules! log_info { - ($loggers:expr, $msg:expr) => { + ($loggers:expr, $($arg:tt)*) => { for l in $loggers { - l.info(format!("{}", $msg)) + l.info(format!($($arg)*)) } }; } #[macro_export] macro_rules! log_warn { - ($loggers:expr, $msg:expr) => { + ($loggers:expr, $($arg:tt)*) => { for l in $loggers { - l.warn(format!("{}", $msg)) + l.warn(format!($($arg)*)) } }; } #[macro_export] macro_rules! log_error { - ($loggers:expr, $msg:expr) => { + ($loggers:expr, $($arg:tt)*) => { for l in $loggers { - l.error(format!("{}", $msg)) + l.error(format!($($arg)*)) } }; } diff --git a/ll-core/src/updates/mod.rs b/ll-core/src/updates/mod.rs index 345ddfd..7aababf 100644 --- a/ll-core/src/updates/mod.rs +++ b/ll-core/src/updates/mod.rs @@ -1,6 +1,5 @@ use { crate::{consts, error::Result}, - reqwest, serde::Deserialize, }; @@ -24,7 +23,7 @@ pub struct UpdateInfo<'l, 'u> { pub url: &'u str, } -pub fn check<'l>(local_version: &'l str, kind: ClientKind) -> Result> { +pub fn check(local_version: &str, kind: ClientKind) -> Result> { let url = format!( "https://raw.githubusercontent.com/olback/library-loader/master/{kind}/Cargo.toml", kind = kind diff --git a/ll-core/src/watcher/event.rs b/ll-core/src/watcher/event.rs index 90d2b56..06d3a5d 100644 --- a/ll-core/src/watcher/event.rs +++ b/ll-core/src/watcher/event.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub enum WatcherEvent { NotifyResult(notify::Result), Stop, diff --git a/ll-core/src/watcher/mod.rs b/ll-core/src/watcher/mod.rs index 39f5149..2441066 100644 --- a/ll-core/src/watcher/mod.rs +++ b/ll-core/src/watcher/mod.rs @@ -1,23 +1,29 @@ use { crate::{ config::Config, cse::CSE, epw::Epw, error::Result, format::Format, log_error, log_if_error, - log_info, logger::Logger, + log_info, log_trace, logger::Logger, }, event::WatcherEvent, notify::{ - event::CreateKind as NotifyCreateKind, EventKind as NotifyEventKind, - Watcher as NotifyWatcher, + event::{ + CreateKind as NotifyCreateKind, ModifyKind as NotifyModifyKind, + RenameMode as NotifyRenameMode, + }, + EventKind as NotifyEventKind, Watcher as NotifyWatcher, }, std::{ ffi::OsString, - path::PathBuf, + path::{Path, PathBuf}, sync::{mpsc, Arc}, thread::{self, JoinHandle}, + time::{Duration, Instant}, }, }; mod event; +const TIME_EVENT_IGNORE: Duration = Duration::from_secs(5); + pub struct Watcher { token: String, watch_path: PathBuf, @@ -29,6 +35,7 @@ pub struct Watcher { notify::RecommendedWatcher, )>, recursive: bool, + ignore_temp: bool, } impl Watcher { @@ -40,6 +47,7 @@ impl Watcher { loggers: Arc::new(loggers), thread: None, recursive: config.settings.recursive, + ignore_temp: config.settings.ignore_temp, }) } @@ -51,51 +59,54 @@ impl Watcher { let mut w: notify::RecommendedWatcher = notify::Watcher::new(move |evt| match ntx.send(WatcherEvent::NotifyResult(evt)) { Ok(_) => {} - Err(e) => log_error!(&*loggers, format!("{:?}", e)), + Err(e) => log_error!(&*loggers, "{:?}", e), })?; let token = self.token.clone(); let formats = Arc::clone(&self.formats); let loggers = Arc::clone(&self.loggers); + let ignore_temp = self.ignore_temp; + let mut last_file: Option<(PathBuf, Instant)> = None; let jh = thread::spawn(move || loop { - match rx.recv() { + let recv_evt = rx.recv(); + log_trace!(&*loggers, "RAW EVENT: {:?}", recv_evt); + match recv_evt { Ok(WatcherEvent::NotifyResult(Ok(event))) => { // log_info!(&*loggers, format!("{:#?}", event)); match event.kind { + NotifyEventKind::Modify(NotifyModifyKind::Name(NotifyRenameMode::Both)) => { + if event.paths.len() == 2 { + log_trace!(&*loggers, "Modify paths: {:#?}", event.paths); + if check_process_file(&event.paths[1], ignore_temp, &last_file) { + log_trace!(&*loggers, "NotifyEventKind::Modify"); + log_info!(&*loggers, "Detected {:?}", &event.paths[1]); + match process(&loggers, &event.paths[1], &token, &formats) { + Ok(()) => { + last_file = + Some((event.paths[1].clone(), Instant::now())); + log_info!(&*loggers, "Done"); + } + Err(e) => { + log_error!(&*loggers, "{:?}", e); + } + } + } + } + } NotifyEventKind::Create(NotifyCreateKind::File) => { - // println!("evt: {:#?}", event); for file in event.paths { - if file.extension().map(|e| e.to_ascii_lowercase()) - == Some(OsString::from("zip")) - { - log_info!(&*loggers, format!("Detected {:?}", file)); - let token = token.clone(); - let formats = Arc::clone(&formats); - let loggers_clone = Arc::clone(&loggers); + if check_process_file(&file, ignore_temp, &last_file) { + log_trace!(&*loggers, "NotifyEventKind::Create"); + log_info!(&*loggers, "Detected {:?}", file); // uuuh std::thread::sleep(std::time::Duration::from_millis(100)); - match (move || -> Result<()> { - let epw = Epw::from_file(file)?; - for res in CSE::new(token, formats).get(epw)? { - match res.save() { - Ok(save_path) => { - log_info!( - &*loggers_clone, - format!("Saved to {:?}", save_path) - ) - } - Err(e) => { - log_error!(&*loggers_clone, e) - } - } - } - Ok(()) - })() { + match process(&loggers, &file, &token, &formats) { Ok(()) => { + last_file = Some((file, Instant::now())); log_info!(&*loggers, "Done"); } Err(e) => { - log_error!(&*loggers, format!("{:?}", e)); + log_error!(&*loggers, "{:?}", e); } } } @@ -106,7 +117,7 @@ impl Watcher { } } Ok(WatcherEvent::NotifyResult(Err(error))) => { - log_error!(&*loggers, format!("{:#?}", error)) + log_error!(&*loggers, "{:#?}", error) } Ok(WatcherEvent::Stop) => break, Err(_recv_error) => { @@ -126,34 +137,100 @@ impl Watcher { self.thread = Some((jh, tx, w)); - log_info!( - &*self.loggers, - format!("Started watching {:?}", self.watch_path) - ); + log_info!(&*self.loggers, "Started watching {:?}", self.watch_path); log_info!(&*self.loggers, "Active formats:"); for f in &*self.formats { - log_info!( - &*self.loggers, - format!("\t{} => {:?}", f.ecad, f.output_path) - ) + log_info!(&*self.loggers, "\t{} => {:?}", f.ecad, f.output_path) } Ok(()) } pub fn stop(&mut self) { - match self.thread.take() { - Some((jh, tx, mut w)) => { - log_if_error!(&*self.loggers, w.unwatch(self.watch_path.as_path())); - log_if_error!(&*self.loggers, tx.send(WatcherEvent::Stop)); - log_if_error!(&*self.loggers, jh.join()); - log_info!( - &*self.loggers, - format!("Stopped watching {:?}", self.watch_path) - ); + if let Some((jh, tx, mut w)) = self.thread.take() { + log_if_error!(&*self.loggers, w.unwatch(self.watch_path.as_path())); + log_if_error!(&*self.loggers, tx.send(WatcherEvent::Stop)); + log_if_error!(&*self.loggers, jh.join()); + log_info!(&*self.loggers, "Stopped watching {:?}", self.watch_path); + } + } +} + +fn process( + loggers: &Arc>>, + file: &Path, + token: &str, + formats: &Arc>, +) -> Result<()> { + let epw = Epw::from_file(file)?; + for res in CSE::new(String::from(token), Arc::clone(formats)).get(epw)? { + match res.save() { + Ok(save_path) => { + log_info!(&**loggers, "Saved to {:?}", save_path) } - None => {} + Err(e) => { + log_error!(&**loggers, "{:?}", e) + } + } + } + Ok(()) +} + +fn check_process_file( + path: &Path, + ignore_temp: bool, + last_file: &Option<(PathBuf, Instant)>, +) -> bool { + const TEMP_FILES: &[&str] = &[".crdownload", ".part", ".download", ".wkdownload"]; + let same_as_last = { + if let Some((f, i)) = last_file { + f == path && Instant::now() - *i < TIME_EVENT_IGNORE + } else { + false } + }; + let is_zip = path.extension().map(|e| e.to_ascii_lowercase()) == Some(OsString::from("zip")); + let is_temp = path.components().any(|c| { + let part = c.as_os_str().to_string_lossy(); + !TEMP_FILES.iter().all(|p| !part.contains(*p)) + }); + + if ignore_temp { + // Must be zip and not a temp file + !same_as_last && is_zip && !is_temp + } else { + // Must be zip, may or may not be a temp file + !same_as_last && is_zip + } +} + +#[cfg(test)] +mod tests { + + use {super::check_process_file, std::path::PathBuf}; + + #[test] + fn check_process_file_test() { + let temp_paths = &[ + PathBuf::from("/a/b/c.zip.crdownload"), + PathBuf::from("/a/b/c.zip.part"), + PathBuf::from("/a/b/c.zip.download"), + PathBuf::from("/a/b/c.zip.crdownload/c.zip"), + PathBuf::from("/a/b/c.zip.part/c.zip"), + PathBuf::from("/a/b/c.zip.download/c.zip"), + ]; + + for p in temp_paths { + assert!(check_process_file(p, true, &None) == false) + } + + let safari_path = PathBuf::from("/a/b/c.zip.download/c.zip"); + assert!(check_process_file(&safari_path, true, &None) == false); + assert!(check_process_file(&safari_path, false, &None) == true); + + let real_path = PathBuf::from("/a/b/c.zip"); + assert!(check_process_file(&real_path, true, &None) == true); + assert!(check_process_file(&real_path, false, &None) == true); } } diff --git a/ll-gui/Cargo.toml b/ll-gui/Cargo.toml index 9627acf..3a77a61 100644 --- a/ll-gui/Cargo.toml +++ b/ll-gui/Cargo.toml @@ -16,3 +16,4 @@ shellexpand = "2" regex = "1" toml = "0.5" serde = { version = "1.0", features = ["derive"] } +ll_core = { path = "../ll-core", package = "library-loader-core" } diff --git a/ll-gui/build/glade.rs b/ll-gui/build/glade.rs index 3825f12..6eb05f3 100644 --- a/ll-gui/build/glade.rs +++ b/ll-gui/build/glade.rs @@ -1,4 +1,3 @@ -use regex; use std::fs; pub fn fix_resource_paths() { @@ -17,7 +16,10 @@ pub fn fix_resource_paths() { .replace("<", "<") .replace(">", ">"), ) - .replace("{{version}}", env!("CARGO_PKG_VERSION")); + .replace( + "{{version}}", + &format!("{} ({})", env!("CARGO_PKG_VERSION"), ll_core::GIT_DESCRIBE), + ); fs::write(GLADE_OUT_PATH, after).unwrap(); } diff --git a/ll-gui/src/ui/mod.rs b/ll-gui/src/ui/mod.rs index d00476c..b4081de 100644 --- a/ll-gui/src/ui/mod.rs +++ b/ll-gui/src/ui/mod.rs @@ -116,7 +116,7 @@ impl Ui { box2.set_spacing(6); box2.set_hexpand(true); box2.set_hexpand_set(true); - let label1 = Label::new(Some(&name)); + let label1 = Label::new(Some(name)); label1.set_halign(Align::Start); label1.set_xalign(0.0); label1.style_context().add_class("format-title"); @@ -207,21 +207,18 @@ impl Ui { let name = inner.add_format_name.text().to_string().trim().to_string(); let format = inner.add_format_format.active_id().map(|f| f.to_string()); let file = inner.add_format_output.file().map(|f| f.path()).flatten(); - match (name.is_empty(), format, file) { - (false, Some(format), Some(file)) => { - let mut conf = inner.config.borrow_mut(); - if conf.formats.get(&name).is_none() { - use std::convert::TryFrom; - conf.formats.insert(name, Format { - format: ECAD::try_from(format.as_str()).expect("Invalid ECAD type in glade file"), - output_path: file.to_str().unwrap().to_string() - }); - drop(inner.tx.send(UiEvent::UpdateFormats)); - } else { - drop(inner.tx.send(UiEvent::ShowInfoBar(format!("Format with name '{}' already exists", name), MessageType::Error))); - } - }, - _ => {} + if let (false, Some(format), Some(file)) = (name.is_empty(), format, file) { + let mut conf = inner.config.borrow_mut(); + if conf.formats.get(&name).is_none() { + use std::convert::TryFrom; + conf.formats.insert(name, Format { + format: ECAD::try_from(format.as_str()).expect("Invalid ECAD type in glade file"), + output_path: file.to_str().unwrap().to_string() + }); + drop(inner.tx.send(UiEvent::UpdateFormats)); + } else { + drop(inner.tx.send(UiEvent::ShowInfoBar(format!("Format with name '{}' already exists", name), MessageType::Error))); + } } } inner.add_format_dialog.hide();