boilerfiles 0.2.0

A CLI for download boilerplate files.
use std::{io::{Cursor}, fs};

use dialoguer::MultiSelect;
use error_chain::error_chain;
use home::home_dir;
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};

error_chain! {
    foreign_links {
        Io(std::io::Error);
        HttpRequest(reqwest::Error);
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct Content {
  name: String,
  download_url: Option<String>,
}

fn main() -> Result<()> {
  let user_repo_arg = std::env::args().nth(1).or(get_config()).expect("Expected \"{user}/{repo}\" as arg or in \"$HOME/.boilerplate\"");
  let (user, repo) = user_repo_arg.split_once("/").expect("Expected arg to be formatted as \"{user}/{repo}\"");
  let path = std::env::args().nth(2).unwrap_or("".to_string());

  let client = reqwest::blocking::Client::builder()
    .user_agent("boilerfiles")
    .build()?;

  let contents = get_contents(&client, &user.to_string(), &repo.to_string(), &path)?;
  let files: Vec<Content> = contents.into_iter().filter(|content| content.download_url.is_some()).collect();
  let file_names: Vec<String> = files.iter().map(|content| content.name.clone()).collect();
  let selections: Vec<&Content> = MultiSelect::new()
      .items(&file_names)
      .interact()?
      .into_iter()
      .map(|index| files.iter().nth(index))
      .filter(|file| file.is_some())
      .map(|x| x.unwrap())
      .collect();

  for remote_file in selections.into_iter() {
    if std::path::Path::new(&[".", &remote_file.name].join("/")).exists() {
      println!("{}: File already exists, skipping", &remote_file.name);
      continue;
    }
    let download_url = remote_file.download_url.clone().unwrap();
    let res = client.get(download_url).send()?;
    match res.status().is_success() {
      true => {
        let bytes = res.bytes()?;
        let mut file = std::fs::File::create(&remote_file.name)?;
        let mut content =  Cursor::new(bytes);
        std::io::copy(&mut content, &mut file)?;
        println!("{}: Download successful ({:.2}KB)", remote_file.name, (file.metadata()?.len() as f64 / 1024.0));
      },
      false => println!("{}: Download failed ({})", remote_file.name, res.status().to_string()),
    }
  }

  Ok(())
}

fn get_contents(client: &Client, user: &String, repo: &String, path: &String) -> Result<Vec<Content>> {
  let url = ["https://api.github.com/repos", user, repo, "contents", path].join("/");
  let res = client
    .get(url)
    .send()?;

  if res.status().is_client_error() || res.status().is_server_error() {
    panic!("{}", res.status().to_string());
  }

  let contents = res.json()?;

  Ok(contents)
}

fn get_config() -> Option<String> {
  let home_dir = home_dir().unwrap();
  let home_path = home_dir.to_str().unwrap();
  let config_path = &[home_path, ".boilerfiles"].join("/");
  let config = fs::read_to_string(config_path);

  return match config {
    Ok(data) => Some(data),
    Err(_error) => None,
  }
}