A cli program to easily handle .gitignore files
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

158 lines
5.6 KiB

//! This module contains structs for working with the github templates
use directories::ProjectDirs;
use log::{debug, trace};
use reqwest::blocking::Client;
use reqwest::header::{ACCEPT, CONTENT_TYPE, USER_AGENT};
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::prelude::*;
use std::io::{BufReader, BufWriter};
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use crate::errors::*;
/// Default user agent string used for api requests
pub const DEFAULT_USER_AGENT: &str = "gitig";
/// Response objects from github api
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::missing_docs_in_private_items)]
pub struct Template {
pub name: String,
pub path: String,
pub sha: String,
pub size: i64,
pub url: String,
#[serde(rename = "html_url")]
pub html_url: String,
#[serde(rename = "git_url")]
pub git_url: String,
#[serde(rename = "download_url")]
pub download_url: Option<String>,
#[serde(rename = "type")]
pub type_field: String,
#[serde(rename = "_links")]
pub links: Links,
}
/// Part of github api response
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::missing_docs_in_private_items)]
pub struct Links {
#[serde(rename = "self")]
pub self_field: String,
pub git: String,
pub html: String,
}
impl Template {
/// Checks if this template file is a gitignore template file
pub fn is_gitignore_template(&self) -> bool { self.name.ends_with(".gitignore") }
/// Returns the name of the file without the .gitignore ending
///
/// if `!self.is_gitignore_template()` the whole `self.name` is returned
pub fn pretty_name(&self) -> &str {
if let Some(dot_index) = self.name.rfind('.') {
return &self.name.get(0..dot_index).unwrap_or_else(|| self.name.as_str());
}
self.name.as_str()
}
}
/// This struct holds the information about the templates available at github
pub struct GithubTemplates {
/// The templates
templates: Vec<Template>,
}
impl GithubTemplates {
/// Loads the templates from the github api
fn from_server() -> Result<GithubTemplates> {
trace!("Loading templates from github api");
let client = Client::new();
let res = client
.get("https://api.github.com/repos/github/gitignore/contents//")
.header(ACCEPT, "application/jsonapplication/vnd.github.v3+json")
.header(CONTENT_TYPE, "application/json")
.header(USER_AGENT, format!("{} {}", DEFAULT_USER_AGENT, env!("CARGO_PKG_VERSION")))
.send()
.chain_err(|| "Error while sending request to query all templates")?;
let body: Vec<Template> = res.json().chain_err(|| "json")?;
debug!("Received and deserialized {} templates", body.len());
trace!("Serializing templates to file cache");
let cache = File::create(Self::cache_path())
.chain_err(|| "Error while creating file cache to write")?;
let writer = BufWriter::new(cache);
serde_json::to_writer(writer, &body)
.chain_err(|| "Error while serialzing templates to file cache")?;
debug!("Serialization of templates completed");
Ok(GithubTemplates { templates: body })
}
/// Returns whether there is an cached json version
fn is_cached() -> bool {
let path = Self::cache_path();
if !path.exists() {
return false;
}
let modified: SystemTime = path
.metadata()
.map(|meta| meta.modified().unwrap_or(SystemTime::UNIX_EPOCH))
.unwrap_or(SystemTime::UNIX_EPOCH);
let max_age = Duration::from_secs(60 * 24 * 2 /* two days */);
if modified.elapsed().unwrap_or(Duration::from_secs(u64::max_value())) > max_age {
debug!("Cache file is too older (> days), won't be used");
return false;
}
true
}
/// Returns the path of the file cache
fn cache_path() -> PathBuf {
let proj = ProjectDirs::from("org", "webschneider", env!("CARGO_PKG_NAME"));
let mut cache: PathBuf = match proj {
Some(p) => p.cache_dir().into(),
None => PathBuf::from("/tmp"),
};
cache.push("templates.json");
debug!("Using cache path for templates: {}", cache.to_str().unwrap_or("err"));
cache
}
/// Reads the templates from the file cache
fn from_cache() -> Result<GithubTemplates> {
trace!("Reading templates from file cache");
if !Self::is_cached() {
debug!("Templates response not yet cached");
return Err("Results are not yet cached".into());
}
let f = File::open(Self::cache_path())
.chain_err(|| "Error while opening cache file for templates")?;
let reader = BufReader::new(f);
let tpls: Vec<Template> = serde_json::from_reader(reader)
.chain_err(|| "Error while reading templates from file cache")?;
debug!("Deserialized {} templates from file cache", tpls.len());
Ok(GithubTemplates { templates: tpls })
}
/// Creates a new struct with templates
pub fn new() -> Result<GithubTemplates> { Self::from_cache().or_else(|_| Self::from_server()) }
/// Returns a list of the template names
pub fn list_names(&self) -> Vec<&str> {
self.templates
.iter()
.filter(|t| t.is_gitignore_template())
.map(Template::pretty_name)
.collect()
}
}