First draft of complete package manager functionality

This commit is contained in:
Federico Terzi 2019-09-27 23:41:15 +02:00
parent 6fa2204aa8
commit b4b89704c3
7 changed files with 267 additions and 35 deletions

View File

@ -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"

View File

@ -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",
}

View File

@ -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;

View File

@ -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<File> {
let espanso_dir = context::get_data_dir();
let lock_file_path = espanso_dir.join("espanso.lock");

View File

@ -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<super::PackageIndex, Box<dyn Error>> {
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<TempDir, Box<dyn Error>> {
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<Package> {
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<String> {
fn list_local_packages_names(&self) -> Vec<String> {
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<InstallResult, Box<dyn Error>> {
// 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<Package> {
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, _| {

View File

@ -31,24 +31,26 @@ pub trait PackageManager {
fn install_package_from_repo(&self, name: &str, repo_url: &str) -> Result<InstallResult, Box<dyn Error>>;
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>;
fn list_local_packages(&self) -> Vec<Package>;
}
#[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<Package>
pub packages: Vec<Package>
}

View File

@ -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<dyn Error>> {
@ -30,10 +29,10 @@ pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box<dyn Error>
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<dyn Error>
mod tests {
use super::*;
use tempfile::TempDir;
use std::path::Path;
use std::fs::create_dir;
#[test]