mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-26 18:14:18 +02:00
340 lines
11 KiB
Rust
340 lines
11 KiB
Rust
use std::env;
|
|
use std::ffi::OsStr;
|
|
use std::fs;
|
|
use std::io::Read;
|
|
use std::iter;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use app_dirs::{get_app_root, AppDataType};
|
|
use flate2::read::GzDecoder;
|
|
use log::debug;
|
|
use reqwest::{blocking::Client, Proxy};
|
|
use std::time::{Duration, SystemTime};
|
|
use tar::Archive;
|
|
use walkdir::{DirEntry, WalkDir};
|
|
|
|
use crate::error::TealdeerError::{self, CacheError, UpdateError};
|
|
use crate::types::{OsType, PathSource};
|
|
|
|
#[derive(Debug)]
|
|
pub struct Cache {
|
|
url: String,
|
|
os: OsType,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct PageLookupResult {
|
|
page_path: PathBuf,
|
|
patch_path: Option<PathBuf>,
|
|
}
|
|
|
|
impl PageLookupResult {
|
|
pub fn with_page(page_path: PathBuf) -> Self {
|
|
Self {
|
|
page_path,
|
|
patch_path: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_optional_patch(mut self, patch_path: Option<PathBuf>) -> Self {
|
|
self.patch_path = patch_path;
|
|
self
|
|
}
|
|
|
|
pub fn paths(&self) -> impl Iterator<Item = &Path> {
|
|
iter::once(self.page_path.as_path()).chain(self.patch_path.as_deref().into_iter())
|
|
}
|
|
}
|
|
|
|
impl Cache {
|
|
pub fn new<S>(url: S, os: OsType) -> Self
|
|
where
|
|
S: Into<String>,
|
|
{
|
|
Self {
|
|
url: url.into(),
|
|
os,
|
|
}
|
|
}
|
|
|
|
/// Return the path to the cache directory.
|
|
pub fn get_cache_dir() -> Result<(PathBuf, PathSource), TealdeerError> {
|
|
// Allow overriding the cache directory by setting the
|
|
// $TEALDEER_CACHE_DIR env variable.
|
|
if let Ok(value) = env::var("TEALDEER_CACHE_DIR") {
|
|
let path = PathBuf::from(value);
|
|
|
|
if path.exists() && path.is_dir() {
|
|
return Ok((path, PathSource::EnvVar));
|
|
}
|
|
return Err(CacheError(
|
|
"Path specified by $TEALDEER_CACHE_DIR \
|
|
does not exist or is not a directory."
|
|
.into(),
|
|
));
|
|
};
|
|
|
|
// Otherwise, fall back to user cache directory.
|
|
match get_app_root(AppDataType::UserCache, &crate::APP_INFO) {
|
|
Ok(dirs) => Ok((dirs, PathSource::OsConvention)),
|
|
Err(_) => Err(CacheError(
|
|
"Could not determine user cache directory.".into(),
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Download the archive
|
|
fn download(&self) -> Result<Vec<u8>, TealdeerError> {
|
|
let mut builder = Client::builder();
|
|
if let Ok(ref host) = env::var("HTTP_PROXY") {
|
|
if let Ok(proxy) = Proxy::http(host) {
|
|
builder = builder.proxy(proxy);
|
|
}
|
|
}
|
|
if let Ok(ref host) = env::var("HTTPS_PROXY") {
|
|
if let Ok(proxy) = Proxy::https(host) {
|
|
builder = builder.proxy(proxy);
|
|
}
|
|
}
|
|
let client = builder.build().unwrap_or_else(|_| Client::new());
|
|
let mut resp = client.get(&self.url).send()?;
|
|
let mut buf: Vec<u8> = vec![];
|
|
let bytes_downloaded = resp.copy_to(&mut buf)?;
|
|
debug!("{} bytes downloaded", bytes_downloaded);
|
|
Ok(buf)
|
|
}
|
|
|
|
/// Decompress and open the archive
|
|
fn decompress<R: Read>(reader: R) -> Archive<GzDecoder<R>> {
|
|
Archive::new(GzDecoder::new(reader))
|
|
}
|
|
|
|
/// Update the pages cache.
|
|
pub fn update(&self) -> Result<(), TealdeerError> {
|
|
// First, download the compressed data
|
|
let bytes: Vec<u8> = self.download()?;
|
|
|
|
// Decompress the response body into an `Archive`
|
|
let mut archive = Self::decompress(&bytes[..]);
|
|
|
|
// Determine paths
|
|
let (cache_dir, _) = Self::get_cache_dir()?;
|
|
|
|
// Make sure that cache directory exists
|
|
debug!("Ensure cache directory {:?} exists", &cache_dir);
|
|
fs::create_dir_all(&cache_dir)
|
|
.map_err(|e| UpdateError(format!("Could not create cache directory: {}", e)))?;
|
|
|
|
// Clear cache directory
|
|
// Note: This is not the best solution. Ideally we would download the
|
|
// archive to a temporary directory and then swap the two directories.
|
|
// But renaming a directory doesn't work across filesystems and Rust
|
|
// does not yet offer a recursive directory copying function. So for
|
|
// now, we'll use this approach.
|
|
Self::clear()?;
|
|
|
|
// Extract archive
|
|
archive
|
|
.unpack(&cache_dir)
|
|
.map_err(|e| UpdateError(format!("Could not unpack compressed data: {}", e)))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Return the duration since the cache directory was last modified.
|
|
pub fn last_update() -> Option<Duration> {
|
|
if let Ok((cache_dir, _)) = Self::get_cache_dir() {
|
|
if let Ok(metadata) = fs::metadata(cache_dir.join("tldr-master")) {
|
|
if let Ok(mtime) = metadata.modified() {
|
|
let now = SystemTime::now();
|
|
return now.duration_since(mtime).ok();
|
|
};
|
|
};
|
|
};
|
|
None
|
|
}
|
|
|
|
/// Return the platform directory.
|
|
fn get_platform_dir(&self) -> Option<&'static str> {
|
|
match self.os {
|
|
OsType::Linux => Some("linux"),
|
|
OsType::OsX => Some("osx"),
|
|
OsType::SunOs => Some("sunos"),
|
|
OsType::Windows => Some("windows"),
|
|
OsType::Other => None,
|
|
}
|
|
}
|
|
|
|
/// Check for pages for a given platform in one of the given languages.
|
|
fn find_page_for_platform(
|
|
page_name: &str,
|
|
cache_dir: &Path,
|
|
platform: &str,
|
|
language_dirs: &[String],
|
|
) -> Option<PathBuf> {
|
|
language_dirs
|
|
.iter()
|
|
.map(|lang_dir| cache_dir.join(lang_dir).join(platform).join(page_name))
|
|
.find(|path| path.exists() && path.is_file())
|
|
}
|
|
|
|
/// Look up custom patch (<name>.patch). If it exists, store it in a variable.
|
|
fn find_patch(patch_name: &str, custom_pages_dir: Option<&Path>) -> Option<PathBuf> {
|
|
custom_pages_dir
|
|
.map(|custom_dir| custom_dir.join(patch_name))
|
|
.filter(|path| path.exists() && path.is_file())
|
|
}
|
|
|
|
/// Search for a page and return the path to it.
|
|
pub fn find_page(
|
|
&self,
|
|
name: &str,
|
|
languages: &[String],
|
|
custom_pages_dir: Option<&Path>,
|
|
) -> Option<PageLookupResult> {
|
|
let page_filename = format!("{}.md", name);
|
|
let patch_filename = format!("{}.patch", name);
|
|
let custom_filename = format!("{}.page", name);
|
|
|
|
// Get cache dir
|
|
let cache_dir = match Self::get_cache_dir() {
|
|
Ok((cache_dir, _)) => cache_dir.join("tldr-master"),
|
|
Err(e) => {
|
|
log::error!("Could not get cache directory: {}", e);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let lang_dirs: Vec<String> = languages
|
|
.iter()
|
|
.map(|lang| {
|
|
if lang == "en" {
|
|
String::from("pages")
|
|
} else {
|
|
format!("pages.{}", lang)
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
// Look up custom page (<name>.page). If it exists, return it directly
|
|
if let Some(config_dir) = custom_pages_dir {
|
|
let custom_page = config_dir.join(custom_filename);
|
|
if custom_page.exists() && custom_page.is_file() {
|
|
return Some(PageLookupResult::with_page(custom_page));
|
|
}
|
|
}
|
|
|
|
let patch_path = Self::find_patch(&patch_filename, custom_pages_dir.as_deref());
|
|
|
|
// Try to find a platform specific path next, append custom patch to it.
|
|
if let Some(pf) = self.get_platform_dir() {
|
|
if let Some(page) =
|
|
Self::find_page_for_platform(&page_filename, &cache_dir, pf, &lang_dirs)
|
|
{
|
|
return Some(PageLookupResult::with_page(page).with_optional_patch(patch_path));
|
|
}
|
|
}
|
|
|
|
// Did not find platform specific results, fall back to "common"
|
|
Self::find_page_for_platform(&page_filename, &cache_dir, "common", &lang_dirs)
|
|
.map(|page| PageLookupResult::with_page(page).with_optional_patch(patch_path))
|
|
}
|
|
|
|
/// Return the available pages.
|
|
pub fn list_pages(&self) -> Result<Vec<String>, TealdeerError> {
|
|
// Determine platforms directory and platform
|
|
let (cache_dir, _) = Self::get_cache_dir()?;
|
|
let platforms_dir = cache_dir.join("tldr-master").join("pages");
|
|
let platform_dir = self.get_platform_dir();
|
|
|
|
// Closure that allows the WalkDir instance to traverse platform
|
|
// specific and common page directories, but not others.
|
|
let should_walk = |entry: &DirEntry| -> bool {
|
|
let file_type = entry.file_type();
|
|
let file_name = match entry.file_name().to_str() {
|
|
Some(name) => name,
|
|
None => return false,
|
|
};
|
|
if file_type.is_dir() {
|
|
if file_name == "common" {
|
|
return true;
|
|
}
|
|
if let Some(platform) = platform_dir {
|
|
return file_name == platform;
|
|
}
|
|
} else if file_type.is_file() {
|
|
return true;
|
|
}
|
|
false
|
|
};
|
|
|
|
// Recursively walk through common and (if applicable) platform specific directory
|
|
let mut pages = WalkDir::new(platforms_dir)
|
|
.min_depth(1) // Skip root directory
|
|
.into_iter()
|
|
.filter_entry(|e| should_walk(e)) // Filter out pages for other architectures
|
|
.filter_map(Result::ok) // Convert results to options, filter out errors
|
|
.filter_map(|e| {
|
|
let path = e.path();
|
|
let extension = &path.extension().and_then(OsStr::to_str).unwrap_or("");
|
|
if e.file_type().is_file() && extension == &"md" {
|
|
path.file_stem()
|
|
.and_then(|stem| stem.to_str().map(|s| s.into()))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<String>>();
|
|
pages.sort();
|
|
pages.dedup();
|
|
Ok(pages)
|
|
}
|
|
|
|
/// Delete the cache directory.
|
|
pub fn clear() -> Result<(), TealdeerError> {
|
|
let (path, _) = Self::get_cache_dir()?;
|
|
if path.exists() && path.is_dir() {
|
|
fs::remove_dir_all(&path).map_err(|_| {
|
|
CacheError(format!(
|
|
"Could not remove cache directory ({}).",
|
|
path.display()
|
|
))
|
|
})?;
|
|
} else if path.exists() {
|
|
return Err(CacheError(format!(
|
|
"Cache path ({}) is not a directory.",
|
|
path.display()
|
|
)));
|
|
} else {
|
|
return Err(CacheError(format!(
|
|
"Cache path ({}) does not exist.",
|
|
path.display()
|
|
)));
|
|
};
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Unit Tests for cache module
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_page_lookup_result_iter_with_patch() {
|
|
let lookup = PageLookupResult::with_page(PathBuf::from("test.page"))
|
|
.with_optional_patch(Some(PathBuf::from("test.patch")));
|
|
let mut iter = lookup.paths();
|
|
assert_eq!(iter.next(), Some(Path::new("test.page")));
|
|
assert_eq!(iter.next(), Some(Path::new("test.patch")));
|
|
assert_eq!(iter.next(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_page_lookup_result_iter_no_patch() {
|
|
let lookup = PageLookupResult::with_page(PathBuf::from("test.page"));
|
|
let mut iter = lookup.paths();
|
|
assert_eq!(iter.next(), Some(Path::new("test.page")));
|
|
assert_eq!(iter.next(), None);
|
|
}
|
|
}
|