Allow overriding cache directory through config

- Config: Add `cache_dir` to `[directories]` section
- Logic to handle legacy env var, config override and default cache dir
  selection is now in the `Config::try_from(RawConfig)` impl
- Deprecate old `TEALDEER_CACHE_DIR` env variable
- Update docs

Co-authored-by: Hans Gaiser <hansg91@gmail.com>
This commit is contained in:
Danilo Bargen 2022-06-16 20:33:04 +02:00
commit 33f0f108d8
5 changed files with 168 additions and 152 deletions

View file

@ -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

View file

@ -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<BufReader<Box<dyn Read>>> {
// 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<S>(url: S, platform: PlatformType) -> Self
pub fn new<S, P>(url: S, platform: PlatformType, cache_dir: P) -> Result<Self>
where
S: Into<String>,
P: Into<PathBuf>,
{
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<Duration> {
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<Duration> {
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<String> = 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<Vec<String>> {
pub fn list_pages(&self, custom_pages_dir: Option<&Path>) -> Vec<String> {
// 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(|| {

View file

@ -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<PathBuf>,
#[serde(default)]
pub custom_pages_dir: Option<PathBuf>,
}
@ -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<PathBuf>,
}
@ -250,34 +254,66 @@ pub struct Config {
pub directories: DirectoriesConfig,
}
impl From<RawConfig> 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<Self> {
// 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<Self> {
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 {

View file

@ -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);
}

View file

@ -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]