From b4b89704c3b689ae042c0802d9adc7a1e13e3cfa Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 27 Sep 2019 23:41:15 +0200 Subject: [PATCH] First draft of complete package manager functionality --- Cargo.toml | 4 +- src/config/mod.rs | 5 +- src/config/runtime.rs | 1 - src/main.rs | 194 ++++++++++++++++++++++++++++++++++++++++- src/package/default.rs | 74 +++++++++++++--- src/package/mod.rs | 18 ++-- src/utils.rs | 6 +- 7 files changed, 267 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9ec8109..297d585 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,12 +28,10 @@ lazy_static = "1.4.0" walkdir = "2.2.9" reqwest = "0.9.20" git2 = {version = "0.10.1", features = ["https"]} +tempfile = "3.1.0" [target.'cfg(unix)'.dependencies] libc = "0.2.62" -[dev-dependencies] -tempfile = "3.1.0" - [build-dependencies] cmake = "0.1.31" \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index 54ec76e..9ae30fd 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -22,7 +22,7 @@ extern crate dirs; use std::path::{Path, PathBuf}; use std::{fs}; use crate::matcher::Match; -use std::fs::{File, create_dir_all, DirEntry}; +use std::fs::{File, create_dir_all}; use std::io::Read; use serde::{Serialize, Deserialize}; use crate::event::KeyModifier; @@ -387,7 +387,6 @@ pub enum ConfigLoadError { InvalidYAML(PathBuf, String), InvalidConfigDirectory, InvalidParameter(PathBuf), - MissingName(PathBuf), NameDuplicate(PathBuf), UnableToCreateDefaultConfig, } @@ -400,7 +399,6 @@ impl fmt::Display for ConfigLoadError { ConfigLoadError::InvalidYAML(path, e) => write!(f, "Error parsing YAML file '{}', invalid syntax: {}", path.to_str().unwrap_or_default(), e), ConfigLoadError::InvalidConfigDirectory => write!(f, "Invalid config directory"), ConfigLoadError::InvalidParameter(path) => write!(f, "Invalid parameter in '{}', use of reserved parameters in used defined configs is not permitted", path.to_str().unwrap_or_default()), - ConfigLoadError::MissingName(path) => write!(f, "The 'name' field is required in user defined configurations, but it's missing in '{}'", path.to_str().unwrap_or_default()), ConfigLoadError::NameDuplicate(path) => write!(f, "Found duplicate 'name' in '{}', please use different names", path.to_str().unwrap_or_default()), ConfigLoadError::UnableToCreateDefaultConfig => write!(f, "Could not generate default config file"), } @@ -415,7 +413,6 @@ impl Error for ConfigLoadError { ConfigLoadError::InvalidYAML(_, _) => "Error parsing YAML file, invalid syntax", ConfigLoadError::InvalidConfigDirectory => "Invalid config directory", ConfigLoadError::InvalidParameter(_) => "Invalid parameter, use of reserved parameters in user defined configs is not permitted", - ConfigLoadError::MissingName(_) => "The 'name' field is required in user defined configurations, but it's missing", ConfigLoadError::NameDuplicate(_) => "Found duplicate 'name' in some configurations, please use different names", ConfigLoadError::UnableToCreateDefaultConfig => "Could not generate default config file", } diff --git a/src/config/runtime.rs b/src/config/runtime.rs index 3bcbe09..3bb0a8b 100644 --- a/src/config/runtime.rs +++ b/src/config/runtime.rs @@ -204,7 +204,6 @@ impl <'a, S: SystemManager> super::ConfigManager<'a> for RuntimeConfigManager<'a #[cfg(test)] mod tests { use super::*; - use std::io::Write; use tempfile::{NamedTempFile, TempDir}; use crate::config::{DEFAULT_CONFIG_FILE_NAME, DEFAULT_CONFIG_FILE_CONTENT}; use std::fs; diff --git a/src/main.rs b/src/main.rs index 08cab2b..58a8c83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,8 @@ use crate::system::SystemManager; use crate::ui::UIManager; use crate::protocol::*; use std::io::{BufReader, BufRead}; +use crate::package::default::DefaultPackageManager; +use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult}; mod ui; mod event; @@ -65,7 +67,12 @@ const VERSION: &'static str = env!("CARGO_PKG_VERSION"); const LOG_FILE: &str = "espanso.log"; fn main() { - let matches = App::new("espanso") + let install_subcommand = SubCommand::with_name("install") + .about("Install a package. Equivalent to 'espanso package install'") + .arg(Arg::with_name("package_name") + .help("Package name")); + + let mut clap_instance = App::new("espanso") .version(VERSION) .author("Federico Terzi") .about("Cross-platform Text Expander written in Rust") @@ -110,7 +117,26 @@ fn main() { .about("Restart the espanso daemon.")) .subcommand(SubCommand::with_name("status") .about("Check if the espanso daemon is running or not.")) - .get_matches(); + + // Package manager + .subcommand(SubCommand::with_name("package") + .about("Espanso package manager commands") + .subcommand(install_subcommand.clone()) + .subcommand(SubCommand::with_name("list") + .about("List all installed packages") + .arg(Arg::with_name("full") + .help("Print all package info") + .long("full"))) + .subcommand(SubCommand::with_name("remove") + .about("Remove an installed package") + .arg(Arg::with_name("package_name") + .help("Package name"))) + .subcommand(SubCommand::with_name("refresh") + .about("Update espanso package index")) + ) + .subcommand(install_subcommand); + + let matches = clap_instance.clone().get_matches(); let log_level = matches.occurrences_of("v") as i32; @@ -192,8 +218,32 @@ fn main() { return; } - // Defaults to start subcommand - start_main(config_set); + if let Some(matches) = matches.subcommand_matches("install") { + install_main(config_set, matches); + return; + } + + if let Some(matches) = matches.subcommand_matches("package") { + if let Some(matches) = matches.subcommand_matches("install") { + install_main(config_set, matches); + return; + } + if let Some(matches) = matches.subcommand_matches("remove") { + remove_package_main(config_set, matches); + return; + } + if let Some(matches) = matches.subcommand_matches("list") { + list_package_main(config_set, matches); + return; + } + if let Some(_) = matches.subcommand_matches("refresh") { + update_index_main(config_set); + return; + } + } + + // Defaults help print + clap_instance.print_long_help().expect("Unable to print help"); } /// Daemon subcommand, start the event loop and spawn a background thread worker @@ -549,6 +599,142 @@ fn unregister_main(config_set: ConfigSet) { sysdaemon::unregister(config_set); } +fn install_main(_config_set: ConfigSet, matches: &ArgMatches) { + let package_name = matches.value_of("package_name").unwrap_or_else(|| { + eprintln!("Missing package name!"); + exit(1); + }); + + let mut package_manager = DefaultPackageManager::new_default(); + + if package_manager.is_index_outdated() { + println!("Updating package index..."); + let res = package_manager.update_index(false); + + match res { + Ok(update_result) => { + match update_result { + UpdateResult::NotOutdated => { + eprintln!("Index was already up to date"); + }, + UpdateResult::Updated => { + println!("Index updated!"); + }, + } + }, + Err(e) => { + eprintln!("{}", e); + exit(2); + }, + } + }else{ + println!("Using cached package index, run 'espanso package refresh' to update it.") + } + + let res = package_manager.install_package(package_name); + + match res { + Ok(install_result) => { + match install_result { + InstallResult::NotFoundInIndex => { + eprintln!("Package not found"); + }, + InstallResult::NotFoundInRepo => { + eprintln!("Package not found in repository, are you sure the folder exist in the repo?"); + }, + InstallResult::UnableToParsePackageInfo => { + eprintln!("Unable to parse Package info from README.md"); + }, + InstallResult::MissingPackageVersion => { + eprintln!("Missing package version"); + }, + InstallResult::AlreadyInstalled => { + eprintln!("{} already installed!", package_name); + }, + InstallResult::Installed => { + println!("{} successfully installed!", package_name); + println!(); + println!("You need to restart espanso for changes to take effect, using:"); + println!(" espanso restart"); + }, + } + }, + Err(e) => { + eprintln!("{}", e); + }, + } +} + +fn remove_package_main(_config_set: ConfigSet, matches: &ArgMatches) { + let package_name = matches.value_of("package_name").unwrap_or_else(|| { + eprintln!("Missing package name!"); + exit(1); + }); + + let package_manager = DefaultPackageManager::new_default(); + + let res = package_manager.remove_package(package_name); + + match res { + Ok(remove_result) => { + match remove_result { + RemoveResult::NotFound => { + eprintln!("{} package was not installed.", package_name); + }, + RemoveResult::Removed => { + println!("{} successfully removed!", package_name); + println!(); + println!("You need to restart espanso for changes to take effect, using:"); + println!(" espanso restart"); + }, + } + }, + Err(e) => { + eprintln!("{}", e); + }, + } +} + +fn update_index_main(_config_set: ConfigSet) { + let mut package_manager = DefaultPackageManager::new_default(); + + let res = package_manager.update_index(true); + + match res { + Ok(update_result) => { + match update_result { + UpdateResult::NotOutdated => { + eprintln!("Index was already up to date"); + }, + UpdateResult::Updated => { + println!("Index updated!"); + }, + } + }, + Err(e) => { + eprintln!("{}", e); + exit(2); + }, + } +} + +fn list_package_main(_config_set: ConfigSet, matches: &ArgMatches) { + let package_manager = DefaultPackageManager::new_default(); + + let list = package_manager.list_local_packages(); + + if matches.is_present("full") { + for package in list.iter() { + println!("{:?}", package); + } + }else{ + for package in list.iter() { + println!("{} - {}", package.name, package.version); + } + } +} + + fn acquire_lock() -> Option { let espanso_dir = context::get_data_dir(); let lock_file_path = espanso_dir.join("espanso.lock"); diff --git a/src/package/default.rs b/src/package/default.rs index 798256d..96ce792 100644 --- a/src/package/default.rs +++ b/src/package/default.rs @@ -22,7 +22,6 @@ use crate::package::{PackageIndex, UpdateResult, Package, InstallResult, RemoveR use std::error::Error; use std::fs::{File, create_dir}; use std::io::{BufReader, BufRead}; -use chrono::{NaiveDateTime, Timelike}; use std::time::{SystemTime, UNIX_EPOCH}; use crate::package::UpdateResult::{NotOutdated, Updated}; use crate::package::InstallResult::{NotFoundInIndex, AlreadyInstalled}; @@ -79,7 +78,7 @@ impl DefaultPackageManager { } fn request_index() -> Result> { - let mut client = reqwest::Client::new(); + let client = reqwest::Client::new(); let request = client.get("https://hub.espanso.org/json/") .header("User-Agent", format!("espanso/{}", crate::VERSION)); @@ -92,13 +91,13 @@ impl DefaultPackageManager { fn clone_repo_to_temp(repo_url: &str) -> Result> { let temp_dir = TempDir::new()?; - let repo = Repository::clone(repo_url, temp_dir.path())?; + let _repo = Repository::clone(repo_url, temp_dir.path())?; Ok(temp_dir) } fn parse_package_from_readme(readme_path: &Path) -> Option { lazy_static! { - static ref FieldRegex: Regex = Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap(); + static ref FIELD_REGEX: Regex = Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap(); } // Read readme line by line @@ -110,7 +109,7 @@ impl DefaultPackageManager { let mut started = false; - for (index, line) in reader.lines().enumerate() { + for (_index, line) in reader.lines().enumerate() { let line = line.unwrap(); if line.contains("---") { if started { @@ -120,7 +119,7 @@ impl DefaultPackageManager { } }else{ if started { - let caps = FieldRegex.captures(&line); + let caps = FIELD_REGEX.captures(&line); if let Some(caps) = caps { let property = caps.get(1); let value = caps.get(2); @@ -142,7 +141,7 @@ impl DefaultPackageManager { return None } - let mut package = Package { + let package = Package { name: fields.get("package_name").unwrap().clone(), title: fields.get("package_title").unwrap().clone(), version: fields.get("package_version").unwrap().clone(), @@ -165,7 +164,7 @@ impl DefaultPackageManager { return 0; } - fn list_local_packages(&self) -> Vec { + fn list_local_packages_names(&self) -> Vec { let dir = fs::read_dir(&self.package_dir); let mut output = Vec::new(); if let Ok(dir) = dir { @@ -184,6 +183,14 @@ impl DefaultPackageManager { output } + + fn cache_local_index(&self) { + if let Some(local_index) = &self.local_index { + let serialized = serde_json::to_string(local_index).expect("Unable to serialize local index"); + let local_index_file = self.data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); + std::fs::write(local_index_file, serialized).expect("Unable to cache local index"); + } + } } impl super::PackageManager for DefaultPackageManager { @@ -202,6 +209,9 @@ impl super::PackageManager for DefaultPackageManager { let updated_index = DefaultPackageManager::request_index()?; self.local_index = Some(updated_index); + // Save the index to file + self.cache_local_index(); + Ok(Updated) }else{ Ok(NotOutdated) @@ -235,7 +245,7 @@ impl super::PackageManager for DefaultPackageManager { fn install_package_from_repo(&self, name: &str, repo_url: &str) -> Result> { // Check if package is already installed - let packages = self.list_local_packages(); + let packages = self.list_local_packages_names(); if packages.iter().any(|p| p == name) { // Package already installed return Ok(AlreadyInstalled); } @@ -281,6 +291,23 @@ impl super::PackageManager for DefaultPackageManager { Ok(Removed) } + + fn list_local_packages(&self) -> Vec { + let mut output = Vec::new(); + + let package_names = self.list_local_packages_names(); + + for name in package_names.iter() { + let package_dir = &self.package_dir.join(name); + let readme_file = package_dir.join("README.md"); + let package = Self::parse_package_from_readme(&readme_file); + if let Some(package) = package { + output.push(package); + } + } + + output + } } #[cfg(test)] @@ -377,6 +404,14 @@ mod tests { assert_eq!(temp.package_manager.update_index(false).unwrap(), UpdateResult::Updated); } + #[test] + fn test_update_index_should_create_file() { + let mut temp = create_temp_package_manager(|_, _| {}); + + assert_eq!(temp.package_manager.update_index(false).unwrap(), UpdateResult::Updated); + assert!(temp.data_dir.path().join(DEFAULT_PACKAGE_INDEX_FILE).exists()) + } + #[test] fn test_get_package_should_be_found() { let mut temp = create_temp_package_manager(|_, data_dir| { @@ -398,14 +433,14 @@ mod tests { } #[test] - fn test_list_local_packages() { + fn test_list_local_packages_names() { let mut temp = create_temp_package_manager(|package_dir, _| { create_dir(package_dir.join("package-1")); create_dir(package_dir.join("package2")); std::fs::write(package_dir.join("dummyfile.txt"), "test"); }); - let packages = temp.package_manager.list_local_packages(); + let packages = temp.package_manager.list_local_packages_names(); assert_eq!(packages.len(), 2); assert!(packages.iter().any(|p| p == "package-1")); assert!(packages.iter().any(|p| p == "package2")); @@ -461,6 +496,23 @@ mod tests { assert_eq!(temp.package_manager.install_package("not-existing").unwrap(), NotFoundInRepo); } + #[test] + fn test_list_local_packages() { + let mut temp = create_temp_package_manager(|_, data_dir| { + let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); + std::fs::write(index_file, INSTALL_PACKAGE_INDEX); + }); + + assert_eq!(temp.package_manager.install_package("dummy-package").unwrap(), Installed); + assert!(temp.package_dir.path().join("dummy-package").exists()); + assert!(temp.package_dir.path().join("dummy-package/README.md").exists()); + assert!(temp.package_dir.path().join("dummy-package/package.yml").exists()); + + let list = temp.package_manager.list_local_packages(); + assert_eq!(list.len(), 1); + assert_eq!(list[0].name, "dummy-package"); + } + #[test] fn test_remove_package() { let mut temp = create_temp_package_manager(|package_dir, _| { diff --git a/src/package/mod.rs b/src/package/mod.rs index f7465e1..3ba505d 100644 --- a/src/package/mod.rs +++ b/src/package/mod.rs @@ -31,24 +31,26 @@ pub trait PackageManager { fn install_package_from_repo(&self, name: &str, repo_url: &str) -> Result>; fn remove_package(&self, name: &str) -> Result>; + + fn list_local_packages(&self) -> Vec; } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Package { - name: String, - title: String, - version: String, - repo: String, - desc: String, - author: String + pub name: String, + pub title: String, + pub version: String, + pub repo: String, + pub desc: String, + pub author: String } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct PackageIndex { #[serde(rename = "lastUpdate")] - last_update: u64, + pub last_update: u64, - packages: Vec + pub packages: Vec } diff --git a/src/utils.rs b/src/utils.rs index 221d3f4..ffd9106 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -19,7 +19,6 @@ use std::path::Path; use std::error::Error; -use walkdir::WalkDir; use std::fs::create_dir; pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box> { @@ -30,10 +29,10 @@ pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box let name = entry.file_name().expect("Error obtaining the filename"); let target_dir = dest_dir.join(name); create_dir(&target_dir)?; - copy_dir(&entry, &target_dir); + copy_dir(&entry, &target_dir)?; }else if entry.is_file() { let target_entry = dest_dir.join(entry.file_name().expect("Error obtaining the filename")); - std::fs::copy(entry, target_entry); + std::fs::copy(entry, target_entry)?; } } @@ -44,7 +43,6 @@ pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box mod tests { use super::*; use tempfile::TempDir; - use std::path::Path; use std::fs::create_dir; #[test]