use crate::{archiver, dirs, download, tool::Tool, version::Version}; use async_std::fs::read_to_string; use octocrab::models::repos::Asset; use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_string_pretty}; use std::path::{Path, PathBuf}; use thiserror::Error; use tracing::{debug, error, info}; use walkdir::{DirEntry, WalkDir}; #[derive(Debug, Error)] pub enum EnvironmentLoadError { #[error("Failed to create the directory '{path}'. {source}")] FailedToCreateDirectory { path: std::path::PathBuf, source: std::io::Error, }, #[error("Unable to read file '{file_path}'. {source:?}")] FileReadError { file_path: std::path::PathBuf, source: std::io::Error, }, #[error("Failed to deserialize file: {file_path}, using {format}. {msg}")] DeserializationError { file_path: std::path::PathBuf, format: String, msg: String, // source: Box, }, } #[derive(Debug, Error)] pub enum EnvironmentError { #[error("Unable to find binary '{expected_file_name}' within folder {search_base_path}")] UnableToFindBinaryError { expected_file_name: String, search_base_path: std::path::PathBuf, }, #[error("Failed to download file '{asset_name}' from '{asset_uri}'")] AssetDownloadError { asset_uri: reqwest::Url, asset_name: String, }, } type Result = std::result::Result; #[derive(Debug, Default, Serialize, Deserialize)] pub struct Environment { pub name: String, pub base_dir: String, #[serde(skip)] base_dir_path: PathBuf, pub tools: Vec, } impl Drop for Environment { fn drop(&mut self) { let out_file = Path::new(&self.base_dir).parent().unwrap(); let out_file = out_file.join(format!("{}.json", &self.name)); debug!("Writing {} environment file to {:?}", &self.name, out_file); match to_string_pretty(&self) { Ok(contents) => match std::fs::write(&out_file, contents) { Ok(_) => debug!("Wrote Environment to file {:?}", out_file), Err(e) => error!("Failed to write Environment file; {}", e), }, Err(e) => error!("Failed to marshal Environment to JSON. {}", e), } } } impl Environment { pub async fn load>(config_dir: P, name: &'_ str) -> Result { let config_dir: PathBuf = config_dir.into(); let env_dir = config_dir.join("envs"); if !env_dir.exists() { match std::fs::create_dir_all(&env_dir) { Ok(_) => {} Err(create_dir_err) => { return Err(EnvironmentLoadError::FailedToCreateDirectory { path: env_dir, source: create_dir_err, }) } } } let env_path = env_dir.join(format!("{}.json", name)); match read_to_string(&env_path).await { Ok(file_contents) => match from_str::(&file_contents) { Ok(mut res) => { res.base_dir = env_dir .join(name) .to_str() .expect("Unable to convert path to a string") .to_string(); Ok(res) } Err(serde_err) => Err(EnvironmentLoadError::DeserializationError { file_path: env_path, format: "json".into(), msg: serde_err.to_string(), }), }, Err(read_err) => match read_err.kind() { std::io::ErrorKind::NotFound => { debug!("Environment file does not exist"); let base_dir: String = env_dir.join(name).to_str().unwrap_or_default().into(); Ok(Environment { name: name.to_string(), base_dir: base_dir.clone(), base_dir_path: Path::new(&base_dir).to_path_buf(), tools: Vec::new(), }) } _ => Err(EnvironmentLoadError::FileReadError { file_path: env_path, source: read_err, }), }, } } pub async fn add_tool( &mut self, name: &'_ str, alias: &'_ str, version: Version, asset: Asset, asset_pattern: &'_ str, file_pattern: &'_ str, ) -> std::result::Result<(), EnvironmentError> { let env_base_path = Path::new(&self.base_dir); let tool_dir = dirs::get_tool_download_dir(env_base_path, name); info!("Actual tools dir: {:?}", tool_dir); let version_tag = version.as_tag(); let tool_version_dir = dirs::get_tool_version_download_dir(env_base_path, name, &version_tag); match download::download_asset(&asset, &tool_version_dir).await { Ok(asset_path) => { info!("Completed downloading {}", asset.browser_download_url); let symlink_dest = dirs::get_tool_link_path(env_base_path, alias); let possible_extractors = archiver::determine_possible_extractors(&asset_path); for extractor in possible_extractors { let extractor_name = &extractor.name(); match archiver::handle_file_extraction( extractor, &asset_path, Some(tool_version_dir.clone()), ) .await { Ok(_) => { info!( "Successfully extracted '{}' using the '{}' extractor", &asset_path.display(), extractor_name ); let binary_file_name = if !file_pattern.is_empty() { file_pattern } else { alias }; if let Some(bin_file) = find_binary(&tool_version_dir, binary_file_name) { create_symlink(&bin_file.into_path(), &symlink_dest); match self.tools.iter_mut().find(|t| t.name == name) { // add to the tools list Some(installed_tool) => { installed_tool.set_current_version(&version); let version_tag = &version.as_tag(); if !installed_tool .installed_versions .iter() .any(|v| v[..] == version_tag[..]) { installed_tool .installed_versions .push(version.as_tag()); info!( "Added new version {} of {} in environment {}", version.as_tag(), name, self.name ); } return Ok(()); } // create a new tool, and add to our list None => { self.tools.push(Tool::new( name, alias, &version, asset_pattern, file_pattern, )); info!( "Added new tool {} in environment {}", name, self.name ); return Ok(()); } }; } else { return Err(EnvironmentError::UnableToFindBinaryError { expected_file_name: binary_file_name.to_string(), search_base_path: tool_version_dir, }); } } Err(e) => { error!( "Failed to extract using '{}' Error: {:?}", extractor_name, e, ); } } } Ok(()) } Err(_) => Err(EnvironmentError::AssetDownloadError { asset_uri: asset.browser_download_url, asset_name: asset.name, }), } } pub fn get_env_path(&self) -> PathBuf { self.base_dir_path.clone() } } fn create_symlink(src: &'_ Path, dest: &'_ Path) { match std::env::consts::OS { "windows" => unimplemented!(), "linux" | "macos" => { if dest.exists() { match std::fs::read_link(dest) { Ok(read_link) => { if *read_link != *dest { // delete the symlink if it isn't pointing to the same file we are trying // to use info!("Removing existing symlink pointing at {:?}", read_link); std::fs::remove_file(dest).unwrap() } } Err(read_err) => panic!("Failed to read symlink. {:?}", read_err), }; } std::fs::create_dir_all(dest.parent().unwrap()).unwrap(); info!("Creating symlink from {:?} to {:?}", src, dest); match std::os::unix::fs::symlink(src, dest) { Ok(_) => (), Err(e) => error!( "Failed to create symlink from {:?} to {:?}. {:?}", src, dest, e ), } } _ => panic!("unknown operating system"), } } fn find_binary(folder: &'_ Path, bin_name: &'_ str) -> Option { WalkDir::new(folder) .into_iter() .filter_map(Result::ok) .find(|entry| entry.file_name() == bin_name) }