diff --git a/docs/src/config_directories.md b/docs/src/config_directories.md index d894e90..b194dbb 100644 --- a/docs/src/config_directories.md +++ b/docs/src/config_directories.md @@ -2,6 +2,19 @@ This section allows overriding some directory paths. +## `cache_dir` + +Override the cache directory. Remember to use an absolute path. Variable +expansion will not be performed on the path. If the directory does not yet +exist, it will be created. + + [directories] + cache_dir = "/home/myuser/.tealdeer-cache/" + +If no `cache_dir` is specified, tealdeer will fall back to a location that +follows OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`. +Use `tldr --show-paths` to show the path that is being used. + ## `custom_pages_dir` Set the directory to be used to look up [custom diff --git a/src/cache.rs b/src/cache.rs index 66b1ef7..35807ea 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -8,15 +8,12 @@ use std::{ }; use anyhow::{ensure, Context, Result}; -use app_dirs::{get_app_root, AppDataType}; use log::debug; use reqwest::{blocking::Client, Proxy}; use walkdir::{DirEntry, WalkDir}; use zip::ZipArchive; -use crate::types::{PathSource, PlatformType}; - -static CACHE_DIR_ENV_VAR: &str = "TEALDEER_CACHE_DIR"; +use crate::types::PlatformType; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; @@ -25,6 +22,7 @@ static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; pub struct Cache { url: String, platform: PlatformType, + cache_dir: PathBuf, } #[derive(Debug)] @@ -54,13 +52,13 @@ impl PageLookupResult { pub fn reader(&self) -> Result>> { // Open page file let page_file = File::open(&self.page_path) - .with_context(|| format!("Could not open page file at {:?}", self.page_path))?; + .with_context(|| format!("Could not open page file at {}", self.page_path.display()))?; // Open patch file let patch_file_opt = match &self.patch_path { Some(path) => Some( File::open(path) - .with_context(|| format!("Could not open patch file at {:?}", path))?, + .with_context(|| format!("Could not open patch file at {}", path.display()))?, ), None => None, }; @@ -89,49 +87,42 @@ pub enum CacheFreshness { } impl Cache { - pub fn new(url: S, platform: PlatformType) -> Self + pub fn new(url: S, platform: PlatformType, cache_dir: P) -> Result where S: Into, + P: Into, { - Self { + // Check whether `cache_dir` exists and is a directory + let cache_dir = cache_dir.into(); + let (cache_dir_exists, cache_dir_is_dir) = cache_dir + .metadata() + .map_or((false, false), |md| (true, md.is_dir())); + ensure!( + !cache_dir_exists || cache_dir_is_dir, + "Cache directory path `{}` is not a directory", + cache_dir.display(), + ); + + // If necessary, create cache directory + if !cache_dir_exists { + // Try to create the complete directory path + fs::create_dir_all(&cache_dir).with_context(|| { + format!( + "Cache directory path `{}` cannot be created", + cache_dir.display(), + ) + })?; + eprintln!( + "Successfully created cache directory path `{}`.", + cache_dir.display(), + ); + } + + Ok(Self { url: url.into(), platform, - } - } - - /// Return the path to the cache directory. - pub fn get_cache_dir() -> Result<(PathBuf, PathSource)> { - // Allow overriding the cache directory by setting the env variable. - if let Ok(value) = env::var(CACHE_DIR_ENV_VAR) { - let path = PathBuf::from(value); - let (path_exists, path_is_dir) = path - .metadata() - .map_or((false, false), |md| (true, md.is_dir())); - ensure!( - !path_exists || path_is_dir, - "Path specified by ${} is not a directory", - CACHE_DIR_ENV_VAR - ); - if !path_exists { - // Try to create the complete directory path. - fs::create_dir_all(&path).with_context(|| { - format!( - "Directory path specified by ${} cannot be created", - CACHE_DIR_ENV_VAR - ) - })?; - eprintln!( - "Successfully created cache directory path `{}`.", - path.to_str().unwrap() - ); - } - return Ok((path, PathSource::EnvVar)); - }; - - // Otherwise, fall back to user cache directory. - let dirs = get_app_root(AppDataType::UserCache, &crate::APP_INFO) - .context("Could not determine user cache directory")?; - Ok((dirs, PathSource::OsConvention)) + cache_dir, + }) } /// Download the archive @@ -171,12 +162,7 @@ impl Cache { .context("Could not decompress downloaded ZIP archive")?; // Determine paths - let (cache_dir, _) = Self::get_cache_dir()?; - let pages_dir = cache_dir.join(TLDR_PAGES_DIR); - - // Make sure that cache directory exists - debug!("Ensure cache directory {:?} exists", &cache_dir); - fs::create_dir_all(&cache_dir).context("Could not create cache directory")?; + let pages_dir = self.cache_dir.join(TLDR_PAGES_DIR); // Clear cache directory // Note: This is not the best solution. Ideally we would download the @@ -184,7 +170,8 @@ impl Cache { // 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().context("Could not clear the cache directory")?; + self.clear() + .context("Could not clear the cache directory")?; // Extract archive archive @@ -195,21 +182,19 @@ impl Cache { } /// Return the duration since the cache directory was last modified. - pub fn last_update() -> Option { - if let Ok((cache_dir, _)) = Self::get_cache_dir() { - if let Ok(metadata) = fs::metadata(cache_dir.join(TLDR_PAGES_DIR)) { - if let Ok(mtime) = metadata.modified() { - let now = SystemTime::now(); - return now.duration_since(mtime).ok(); - }; + pub fn last_update(&self) -> Option { + if let Ok(metadata) = fs::metadata(self.cache_dir.join(TLDR_PAGES_DIR)) { + if let Ok(mtime) = metadata.modified() { + let now = SystemTime::now(); + return now.duration_since(mtime).ok(); }; }; None } /// Return the freshness of the cache (fresh, stale or missing). - pub fn freshness() -> CacheFreshness { - match Cache::last_update() { + pub fn freshness(&self) -> CacheFreshness { + match self.last_update() { Some(ago) if ago > crate::config::MAX_CACHE_AGE => CacheFreshness::Stale(ago), Some(_) => CacheFreshness::Fresh, None => CacheFreshness::Missing, @@ -258,15 +243,8 @@ impl Cache { 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_PAGES_DIR), - Err(e) => { - log::error!("Could not get cache directory: {}", e); - return None; - } - }; - + // Determine directories + let cache_dir = self.cache_dir.join(TLDR_PAGES_DIR); let lang_dirs: Vec = languages .iter() .map(|lang| { @@ -302,10 +280,9 @@ impl Cache { } /// Return the available pages. - pub fn list_pages(&self, custom_pages_dir: Option<&Path>) -> Result> { + pub fn list_pages(&self, custom_pages_dir: Option<&Path>) -> Vec { // Determine platforms directory and platform - let (cache_dir, _) = Self::get_cache_dir()?; - let platforms_dir = cache_dir.join(TLDR_PAGES_DIR).join("pages"); + let platforms_dir = self.cache_dir.join(TLDR_PAGES_DIR).join("pages"); let platform_dir = self.get_platform_dir(); // Closure that allows the WalkDir instance to traverse platform @@ -367,29 +344,27 @@ impl Cache { pages.sort(); pages.dedup(); - Ok(pages) + pages } /// Delete the cache directory. - pub fn clear() -> Result<()> { - let (path, _) = Self::get_cache_dir()?; - + pub fn clear(&self) -> Result<()> { // Check preconditions ensure!( - path.exists(), + self.cache_dir.exists(), "Cache path ({}) does not exist.", - path.display(), + self.cache_dir.display(), ); ensure!( - path.is_dir(), + self.cache_dir.is_dir(), "Cache path ({}) is not a directory.", - path.display() + self.cache_dir.display(), ); // Delete old tldr-pages cache location as well if present // TODO: To be removed in the future for pages_dir_name in [TLDR_PAGES_DIR, TLDR_OLD_PAGES_DIR] { - let pages_dir = path.join(pages_dir_name); + let pages_dir = self.cache_dir.join(pages_dir_name); if pages_dir.exists() { fs::remove_dir_all(&pages_dir).with_context(|| { diff --git a/src/config.rs b/src/config.rs index 74ba195..f5e996a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,7 @@ use std::{ time::Duration, }; -use anyhow::{ensure, Context, Result}; +use anyhow::{bail, ensure, Context, Result}; use app_dirs::{get_app_root, AppDataType}; use log::debug; use serde_derive::{Deserialize, Serialize}; @@ -164,6 +164,8 @@ impl Default for RawUpdatesConfig { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] struct RawDirectoriesConfig { + #[serde(default)] + pub cache_dir: Option, #[serde(default)] pub custom_pages_dir: Option, } @@ -171,6 +173,7 @@ struct RawDirectoriesConfig { impl Default for RawDirectoriesConfig { fn default() -> Self { Self { + cache_dir: None, custom_pages_dir: get_app_root(AppDataType::UserData, &crate::APP_INFO) .map(|path| { // Note: The `join("")` call ensures that there's a trailing slash @@ -239,6 +242,7 @@ pub struct UpdatesConfig { #[derive(Clone, Debug, PartialEq, Eq)] pub struct DirectoriesConfig { + pub cache_dir: PathBuf, pub custom_pages_dir: Option, } @@ -250,34 +254,66 @@ pub struct Config { pub directories: DirectoriesConfig, } -impl From for Config { - fn from(raw_config: RawConfig) -> Self { - Self { - style: StyleConfig { - command_name: raw_config.style.command_name.into(), - description: raw_config.style.description.into(), - example_text: raw_config.style.example_text.into(), - example_code: raw_config.style.example_code.into(), - example_variable: raw_config.style.example_variable.into(), - }, - display: DisplayConfig { - compact: raw_config.display.compact, - use_pager: raw_config.display.use_pager, - }, - updates: UpdatesConfig { - auto_update: raw_config.updates.auto_update, - auto_update_interval: Duration::from_secs( - raw_config.updates.auto_update_interval_hours * 3600, - ), - }, - directories: DirectoriesConfig { - custom_pages_dir: raw_config.directories.custom_pages_dir, - }, - } - } -} - impl Config { + /// Convert a `RawConfig` to a high-level `Config`. + /// + /// For this, some values need to be converted to other types and some + /// defaults need to be set (sometimes based on env variables). + fn from_raw(raw_config: RawConfig) -> Result { + // Style config + let style = StyleConfig { + command_name: raw_config.style.command_name.into(), + description: raw_config.style.description.into(), + example_text: raw_config.style.example_text.into(), + example_code: raw_config.style.example_code.into(), + example_variable: raw_config.style.example_variable.into(), + }; + + // Display config + let display = DisplayConfig { + compact: raw_config.display.compact, + use_pager: raw_config.display.use_pager, + }; + + // Updates config + let updates = UpdatesConfig { + auto_update: raw_config.updates.auto_update, + auto_update_interval: Duration::from_secs( + raw_config.updates.auto_update_interval_hours * 3600, + ), + }; + + // Directories config + let cache_dir_env_var = "TEALDEER_CACHE_DIR"; + let cache_dir = if let Ok(env_var) = env::var(cache_dir_env_var) { + // For backwards compatibility reasons, the cache directory can be + // overridden using an env variable. This is deprecated and will be + // phased out in the future. + eprintln!("Warning: The ${} env variable is deprecated, use the `cache_dir` option in the config file instead.", cache_dir_env_var); + PathBuf::from(env_var) + } else if let Some(config_value) = raw_config.directories.cache_dir { + // If the user explicitly configured a cache directory, use that. + config_value + } else if let Ok(default_dir) = get_app_root(AppDataType::UserCache, &crate::APP_INFO) { + // Otherwise, fall back to the default user cache directory. + default_dir + } else { + // If everything fails, give up + bail!("Could not determine user cache directory"); + }; + let directories = DirectoriesConfig { + cache_dir, + custom_pages_dir: raw_config.directories.custom_pages_dir, + }; + + Ok(Self { + style, + display, + updates, + directories, + }) + } + pub fn load(enable_styles: bool) -> Result { debug!("Loading config"); @@ -301,7 +337,7 @@ impl Config { }; // Convert to config - let mut config = Self::from(raw_config); + let mut config = Self::from_raw(raw_config).context("Could not process raw config")?; // Potentially override styles if !enable_styles { diff --git a/src/main.rs b/src/main.rs index 6752358..83cb673 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,11 +59,13 @@ const ARCHIVE_URL: &str = "https://tldr.sh/assets/tldr.zip"; /// The cache should be updated if it was explicitly requested, /// or if an automatic update is due and allowed. -fn should_update_cache(args: &Args, config: &Config) -> bool { +fn should_update_cache(cache: &Cache, args: &Args, config: &Config) -> bool { args.update || (!args.no_auto_update && config.updates.auto_update - && Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval)) + && cache + .last_update() + .map_or(true, |ago| ago >= config.updates.auto_update_interval)) } #[derive(PartialEq)] @@ -73,8 +75,8 @@ enum CheckCacheResult { } /// Check the cache for freshness. If it's stale or missing, show a warning. -fn check_cache(args: &Args, enable_styles: bool) -> CheckCacheResult { - match Cache::freshness() { +fn check_cache(cache: &Cache, args: &Args, enable_styles: bool) -> CheckCacheResult { + match cache.freshness() { CacheFreshness::Fresh => CheckCacheResult::CacheFound, CacheFreshness::Stale(_) if args.quiet => CheckCacheResult::CacheFound, CacheFreshness::Stale(age) => { @@ -109,8 +111,8 @@ fn check_cache(args: &Args, enable_styles: bool) -> CheckCacheResult { } /// Clear the cache -fn clear_cache(quietly: bool, enable_styles: bool) { - Cache::clear().unwrap_or_else(|e| { +fn clear_cache(cache: &Cache, quietly: bool, enable_styles: bool) { + cache.clear().unwrap_or_else(|e| { print_error(enable_styles, &e.context("Could not clear cache")); process::exit(1); }); @@ -157,28 +159,15 @@ fn show_paths(config: &Config) { ); let config_path = get_config_path().map_or_else( |e| format!("[Error: {}]", e), - |(path, _)| path.to_str().unwrap_or("[Invalid]").to_string(), - ); - let cache_dir = Cache::get_cache_dir().map_or_else( - |e| format!("[Error: {}]", e), - |(mut path, source)| { - path.push(""); // Trailing path separator - match path.to_str() { - Some(path) => format!("{} ({})", path, source), - None => "[Invalid]".to_string(), - } - }, - ); - let pages_dir = Cache::get_cache_dir().map_or_else( - |e| format!("[Error: {}]", e), - |(mut path, _)| { - path.push(TLDR_PAGES_DIR); - path.push(""); // Trailing path separator - path.into_os_string() - .into_string() - .unwrap_or_else(|_| "[Invalid]".to_string()) - }, + |(path, _)| path.display().to_string(), ); + let cache_dir = config.directories.cache_dir.display(); + let pages_dir = { + let mut path = config.directories.cache_dir.clone(); + path.push(TLDR_PAGES_DIR); + path.push(""); // Trailing path separator + path.display().to_string() + }; let custom_pages_dir = config.directories.custom_pages_dir.as_deref().map_or_else( || "[None]".to_string(), |path| { @@ -339,15 +328,19 @@ fn main() { } // Initialize cache - let cache = Cache::new(ARCHIVE_URL, platform); + let cache = + Cache::new(ARCHIVE_URL, platform, &config.directories.cache_dir).unwrap_or_else(|e| { + print_error(enable_styles, &e.context("Could not initialize cache")); + process::exit(1); + }); // Clear cache, pass through if args.clear_cache { - clear_cache(args.quiet, enable_styles); + clear_cache(&cache, args.quiet, enable_styles); } // Cache update, pass through - let cache_updated = if should_update_cache(&args, &config) { + let cache_updated = if should_update_cache(&cache, &args, &config) { update_cache(&cache, args.quiet, enable_styles); true } else { @@ -357,23 +350,19 @@ fn main() { // Check cache presence and freshness if !cache_updated && (args.list || !args.command.is_empty()) - && check_cache(&args, enable_styles) == CheckCacheResult::CacheMissing + && check_cache(&cache, &args, enable_styles) == CheckCacheResult::CacheMissing { process::exit(1); } // List cached commands and exit if args.list { - // Get list of pages - let pages = cache - .list_pages(config.directories.custom_pages_dir.as_deref()) - .unwrap_or_else(|e| { - print_error(enable_styles, &e.context("Could not get list of pages")); - process::exit(1); - }); - - // Print pages - println!("{}", pages.join("\n")); + println!( + "{}", + cache + .list_pages(config.directories.custom_pages_dir.as_deref()) + .join("\n") + ); process::exit(0); } diff --git a/tests/lib.rs b/tests/lib.rs index 6e66764..71efe09 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -263,9 +263,12 @@ fn test_cache_location_not_a_directory() { .assert() .failure() .stderr(contains(format!( - "Path specified by ${} is not a directory", - CACHE_DIR_ENV_VAR - ))); + "Cache directory path `{}` is not a directory", + internal_file.display(), + ))) + .stderr(contains( + "Warning: The $TEALDEER_CACHE_DIR env variable is deprecated", + )); } #[test]