Skip to content

Commit

Permalink
[#33] Better end-of-sync output
Browse files Browse the repository at this point in the history
Resolves #33
  • Loading branch information
chshersh committed Sep 13, 2022
1 parent 56168b1 commit ffc96c7
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 241 deletions.
6 changes: 3 additions & 3 deletions src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ pub struct ConfigAsset {
/// Defaults to `repo` if not specified
pub exe_name: Option<String>,

/// Name of the specific asset to download
pub asset_name: AssetName,

/// Release tag to download
/// Defaults to the latest release
pub tag: Option<String>,

/// Name of the specific asset to download
pub asset_name: AssetName,
}

impl Config {
Expand Down
1 change: 1 addition & 0 deletions src/infra.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod client;
103 changes: 103 additions & 0 deletions src/infra/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::env;
use std::error::Error;
use std::io::Read;

use crate::model::release::{Asset, Release};

/// GitHub API client to handle all API requests
pub struct Client {
pub owner: String,
pub repo: String,
pub version: String,
}

impl Client {
fn release_url(&self) -> String {
format!(
"https://api.github.com/repos/{owner}/{repo}/releases/{version}",
owner = self.owner,
repo = self.repo,
version = self.version,
)
}

fn asset_url(&self, asset_id: u32) -> String {
format!(
"https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id}",
owner = self.owner,
repo = self.repo,
asset_id = asset_id
)
}

pub fn fetch_release_info(&self) -> Result<Release, Box<dyn Error>> {
let release_url = self.release_url();

let req = add_auth_header(
ureq::get(&release_url)
.set("Accept", "application/vnd.github+json")
.set("User-Agent", "chshersh/tool-sync-0.2.0"),
);

let release: Release = req.call()?.into_json()?;

Ok(release)
}

pub fn get_asset_stream(
&self,
asset: &Asset,
) -> Result<Box<dyn Read + Send + Sync>, ureq::Error> {
let asset_url = self.asset_url(asset.id);

let req = add_auth_header(
ureq::get(&asset_url)
.set("Accept", "application/octet-stream")
.set("User-Agent", "chshersh/tool-sync-0.2.0"),
);

Ok(req.call()?.into_reader())
}
}

fn add_auth_header(req: ureq::Request) -> ureq::Request {
match env::var("GITHUB_TOKEN") {
Err(_) => req,
Ok(token) => req.set("Authorization", &format!("token {}", token)),
}
}

#[cfg(test)]
mod tests {
use super::*;

use crate::model::tool::ToolInfoTag;

#[test]
fn release_url_with_latest_tag_is_correct() {
let client = Client {
owner: String::from("OWNER"),
repo: String::from("REPO"),
version: ToolInfoTag::Latest.to_str_version(),
};

assert_eq!(
client.release_url(),
"https://api.github.com/repos/OWNER/REPO/releases/latest"
);
}

#[test]
fn release_url_with_specific_tag_is_correct() {
let client = Client {
owner: String::from("OWNER"),
repo: String::from("REPO"),
version: ToolInfoTag::Specific(String::from("SPECIFIC_TAG")).to_str_version(),
};

assert_eq!(
client.release_url(),
"https://api.github.com/repos/OWNER/REPO/releases/tags/SPECIFIC_TAG"
);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod config;
mod err;
mod infra;
mod model;
mod sync;

Expand Down
2 changes: 1 addition & 1 deletion src/model/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub struct Release {
pub assets: Vec<Asset>,
}

#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Clone)]
pub struct Asset {
pub id: u32,
pub name: String,
Expand Down
46 changes: 44 additions & 2 deletions src/model/tool.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use super::release::Asset;
use crate::infra::client::Client;
use crate::model::asset_name::AssetName;

#[derive(Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -59,9 +61,49 @@ pub struct ToolInfo {
/// Executable name inside the .tar.gz or .zip archive
pub exe_name: String,

/// Version tag
pub tag: ToolInfoTag,

/// Asset name depending on the OS
pub asset_name: AssetName,
}

/// Version tag
pub tag: ToolInfoTag,
impl ToolInfo {
pub fn select_asset(&self, assets: &[Asset]) -> Result<Asset, String> {
match self.asset_name.get_name_by_os() {
None => Err(String::from(
"Don't know the asset name for this OS: specify it explicitly in the config",
)),
Some(asset_name) => {
let asset = assets.iter().find(|&asset| asset.name.contains(asset_name));

match asset {
None => Err(format!("No asset matching name: {}", asset_name)),
Some(asset) => Ok(asset.clone()),
}
}
}
}
}

/// All information about the tool, needed to download its asset after fetching
/// the release and asset info. Values of this type are created in
/// `src/sync/prefetch.rs` from `ToolInfo`.
pub struct ToolAsset {
/// Name of the tool (e.g. "ripgrep" or "exa")
pub tool_name: String,

/// Specific git tag (e.g. "v3.4.2")
/// This value is the result of `ToolInfoTag::to_str_version` so "latest"
/// **can't** be here.
pub tag: String,

/// Executable name inside the .tar.gz or .zip archive
pub exe_name: String,

/// The selected asset
pub asset: Asset,

/// GitHub API client that produces the stream for downloading the asset
pub client: Client,
}
70 changes: 51 additions & 19 deletions src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,29 @@ mod configure;
mod db;
mod download;
mod install;
mod prefetch;
mod progress;

use console::Emoji;

use crate::config::schema::Config;
use crate::sync::install::Installer;
use crate::sync::progress::SyncProgress;

use self::install::Installer;
use self::prefetch::prefetch;
use self::progress::SyncProgress;
use self::progress::ToolPair;

pub fn sync(config: Config) {
if config.tools.is_empty() {
eprintln!(
r#"No tools to sync. Have you configured 'tool-sync'?
no_tools_message()
} else {
sync_tools(config)
}
}

fn no_tools_message() {
eprintln!(
r#"No tools to sync. Have you configured 'tool-sync'?
Put the following into the $HOME/.tool.toml file for the simplest configuration:
Expand All @@ -27,21 +40,40 @@ Put the following into the $HOME/.tool.toml file for the simplest configuration:
For more details, refer to the official documentation:
* https://github.com/chshersh/tool-sync#tool-sync"#
);
} else {
let store_directory = config.ensure_store_directory();

let tools: Vec<String> = config.tools.keys().cloned().collect();
let tags: Vec<String> = config
.tools
.values()
.map(|config_asset| config_asset.tag.clone().unwrap_or_else(|| "latest".into()))
.collect();
let sync_progress = SyncProgress::new(tools, tags);
let installer = Installer::mk(store_directory, sync_progress);

for (tool_name, config_asset) in config.tools.iter() {
installer.install(tool_name, config_asset);
);
}

const DONE: Emoji<'_, '_> = Emoji("✨ ", "* ");
const DIRECTORY: Emoji<'_, '_> = Emoji("📁 ", "* ");

fn sync_tools(config: Config) {
let store_directory = config.ensure_store_directory();
let tool_assets = prefetch(config.tools);

let tool_pairs = tool_assets
.iter()
.map(|ta| ToolPair {
name: &ta.tool_name,
tag: &ta.tag,
})
.collect();

let sync_progress = SyncProgress::new(tool_pairs);
let installer = Installer::mk(store_directory.as_path(), sync_progress);

let mut installed_tools: u64 = 0;

for tool_asset in tool_assets {
let is_successs = installer.install(tool_asset);
if is_successs {
installed_tools += 1
}
}

eprintln!("{} Successfully installed {} tools!", DONE, installed_tools);
eprintln!(
"{} Installation directory: {}",
DIRECTORY,
store_directory.display()
);
}
Loading

0 comments on commit ffc96c7

Please sign in to comment.