First draft of complete package manager functionality
This commit is contained in:
parent
6fa2204aa8
commit
b4b89704c3
|
@ -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"
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
194
src/main.rs
194
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<File> {
|
||||
let espanso_dir = context::get_data_dir();
|
||||
let lock_file_path = espanso_dir.join("espanso.lock");
|
||||
|
|
|
@ -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, _| {
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue
Block a user