Merge pull request #115 from ilai-deutel/automatic-updates

Implement automatic cache updates
This commit is contained in:
Danilo Bargen 2020-06-26 16:59:28 +02:00 committed by GitHub
commit d446e50813
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 209 additions and 99 deletions

19
Cargo.lock generated
View file

@ -194,7 +194,7 @@ dependencies = [
[[package]]
name = "filetime"
version = "0.2.8"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
@ -996,7 +996,7 @@ name = "tar"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"filetime 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
"xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1012,6 +1012,7 @@ dependencies = [
"docopt 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
"flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"pager 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1022,7 +1023,6 @@ dependencies = [
"tar 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
"utime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"xdg 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1180,16 +1180,6 @@ dependencies = [
"percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "utime"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "vcpkg"
version = "0.2.8"
@ -1420,7 +1410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum errno 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2a071601ed01b988f896ab14b95e67335d1eeb50190932a1320f7fe3cadc84e"
"checksum errno-dragonfly 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067"
"checksum escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19db1f7e74438642a5018cdf263bb1325b2e792f02dd0a3ca6d6c0f0d7b1d5a5"
"checksum filetime 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1ff6d4dab0aa0c8e6346d46052e93b13a16cf847b54ed357087c35011048cc7d"
"checksum filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "affc17579b132fc2461adf7c575cc6e8b134ebca52c51f5411388965227dc695"
"checksum flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6bd6d6f4752952feb71363cffc9ebac9411b75b87c6ab6058c40c8900cf43c0f"
"checksum float-cmp 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "da62c4f1b81918835a8c6a484a397775fff5953fe83529afd51b05f5c6a6617d"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
@ -1533,7 +1523,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
"checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
"checksum utime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "055058552ca15c566082fc61da433ae678f78986a6f16957e33162d1b218792a"
"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"

View file

@ -37,7 +37,7 @@ assert_cmd = "0.10"
escargot = "0.3"
predicates = "1.0"
tempdir = "^0.3"
utime = "0.2.0"
filetime = "0.2.10"
[features]
logging = ["env_logger"]

View file

@ -208,6 +208,31 @@ Set this to enforce more compact output, where empty lines are stripped out
compact = true
### Automatic updates
tealdeer can refresh the cache automatically when it is outdated. This
behavior can be configured in the `updates` section and is disabled by
default.
#### `auto_update`
Specifies whether the auto-update feature should be enabled (defaults to
`false`).
[updates]
auto_update = true
#### `auto_update_interval_hours`
Duration, since the last cache update, after which the cache will be
refreshed (defaults to 720 hours). This parameter is ignored if `auto_update`
is set to `false`.
[updates]
auto_update = true
auto_update_interval_hours = 24
## Autocompletion
- *Bash*: copy `bash_tealdeer` to `/usr/share/bash-completion/completions/tldr`

View file

@ -2,6 +2,7 @@ use std::env;
use std::fs;
use std::io::{Error as IoError, Read, Write};
use std::path::PathBuf;
use std::time::Duration;
use ansi_term::{Color, Style};
use app_dirs::{get_app_root, AppDataType};
@ -12,6 +13,8 @@ use toml;
use crate::error::TealdeerError::{self, ConfigError};
pub const CONFIG_FILE_NAME: &str = "config.toml";
pub const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days
const DEFAULT_UPDATE_INTERVAL_HOURS: u64 = MAX_CACHE_AGE.as_secs() / 3600; // 30 days
fn default_underline() -> bool {
false
@ -116,12 +119,38 @@ struct RawDisplayConfig {
pub use_pager: bool,
}
/// Serde doesn't support default values yet (tracking issue:
/// https://github.com/serde-rs/serde/issues/368), so we need to wrap DEFAULT_UPDATE_INTERVAL_HOURS
/// in a function to be able to use #[serde(default = ...)]
const fn default_auto_update_interval_hours() -> u64 {
DEFAULT_UPDATE_INTERVAL_HOURS
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct RawUpdatesConfig {
#[serde(default)]
pub auto_update: bool,
#[serde(default = "default_auto_update_interval_hours")]
pub auto_update_interval_hours: u64,
}
impl Default for RawUpdatesConfig {
fn default() -> Self {
Self {
auto_update: false,
auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS,
}
}
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
struct RawConfig {
#[serde(default)]
style: RawStyleConfig,
#[serde(default)]
display: RawDisplayConfig,
#[serde(default)]
updates: RawUpdatesConfig,
}
impl RawConfig {
@ -134,7 +163,6 @@ impl RawConfig {
raw_config.style.example_code.foreground = Some(RawColor::Cyan);
raw_config.style.example_variable.foreground = Some(RawColor::Cyan);
raw_config.style.example_variable.underline = true;
raw_config.display.use_pager = false;
raw_config
}
@ -155,10 +183,17 @@ pub struct DisplayConfig {
pub use_pager: bool,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct UpdatesConfig {
pub auto_update: bool,
pub auto_update_interval: Duration,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Config {
pub style: StyleConfig,
pub display: DisplayConfig,
pub updates: UpdatesConfig,
}
impl From<RawConfig> for Config {
@ -175,6 +210,12 @@ impl From<RawConfig> for Config {
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,
),
},
}
}
}

View file

@ -21,7 +21,6 @@ use std::io::BufReader;
use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::process;
use std::time::Duration;
use ansi_term::Color;
use app_dirs::AppInfo;
@ -38,7 +37,7 @@ mod tokenizer;
mod types;
use crate::cache::Cache;
use crate::config::{get_config_path, make_default_config, Config};
use crate::config::{get_config_path, make_default_config, Config, MAX_CACHE_AGE};
use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError};
use crate::formatter::print_lines;
use crate::tokenizer::Tokenizer;
@ -86,7 +85,6 @@ To render a local file (for testing):
$ tldr --render /path/to/file.md
";
const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz";
const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days
#[cfg(not(target_os = "windows"))]
const PAGER_COMMAND: &str = "less -R";
@ -108,24 +106,11 @@ struct Args {
}
/// Print page by path
fn print_page(path: &Path, enable_markdown: bool, enable_styles: bool) -> Result<(), String> {
fn print_page(path: &Path, enable_markdown: bool, config: &Config) -> Result<(), String> {
// Open file
let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?;
let reader = BufReader::new(file);
// Look up config file, if none is found fall back to default config.
let config = match Config::load(enable_styles) {
Ok(config) => config,
Err(ConfigError(msg)) => {
eprintln!("Could not load config: {}", msg);
process::exit(1);
}
Err(e) => {
eprintln!("Could not load config: {}", e);
process::exit(1);
}
};
if enable_markdown {
// Print the raw markdown of the file.
for line in reader.lines() {
@ -142,59 +127,42 @@ fn print_page(path: &Path, enable_markdown: bool, enable_styles: bool) -> Result
/// Set up display pager
#[cfg(not(target_os = "windows"))]
fn configure_pager(args: &Args, enable_styles: bool) {
// Flags have precedence
if args.flag_pager {
Pager::with_default_pager(PAGER_COMMAND).setup();
return;
}
// Then check config
let config = match Config::load(enable_styles) {
Ok(config) => config,
Err(ConfigError(msg)) => {
eprintln!("Could not load config: {}", msg);
process::exit(1);
}
Err(e) => {
eprintln!("Could not load config: {}", e);
process::exit(1);
}
};
if config.display.use_pager {
Pager::with_default_pager(PAGER_COMMAND).setup();
}
fn configure_pager() {
Pager::with_default_pager(PAGER_COMMAND).setup();
}
#[cfg(target_os = "windows")]
fn configure_pager(_args: &Args, _enable_styles: bool) {
fn configure_pager() {
eprintln!("Warning: -p / --pager flag not available on Windows!");
}
fn should_update_cache(args: &Args, config: &Config) -> bool {
args.flag_update
|| (config.updates.auto_update
&& Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval))
}
/// Check the cache for freshness
fn check_cache(args: &Args) {
if !args.flag_update {
match Cache::last_update() {
Some(ago) if ago > MAX_CACHE_AGE => {
if args.flag_quiet {
return;
}
eprintln!(
"{}",
Color::Yellow.paint(format!(
"The cache hasn't been updated for more than {} days.\n\
match Cache::last_update() {
Some(ago) if ago > MAX_CACHE_AGE => {
if args.flag_quiet {
return;
}
eprintln!(
"{}",
Color::Yellow.paint(format!(
"The cache hasn't been updated for more than {} days.\n\
You should probably run `tldr --update` soon.",
MAX_CACHE_AGE.as_secs() / 24 / 3600
))
);
}
None => {
eprintln!("Cache not found. Please run `tldr --update`.");
process::exit(1);
}
_ => {}
MAX_CACHE_AGE.as_secs() / 24 / 3600
))
);
}
None => {
eprintln!("Cache not found. Please run `tldr --update`.");
process::exit(1);
}
_ => {}
};
}
@ -320,14 +288,38 @@ fn main() {
process::exit(0);
}
// Show config file and path, pass through
if args.flag_config_path {
show_config_path();
}
// Create a basic config and exit
if args.flag_seed_config {
create_config_and_exit();
}
// Determine the usage of styles
#[cfg(target_os = "windows")]
let enable_styles = ansi_term::enable_ansi_support().is_ok();
#[cfg(not(target_os = "windows"))]
let enable_styles = true;
// Configure pager
configure_pager(&args, enable_styles);
// Look up config file, if none is found fall back to default config.
let config = match Config::load(enable_styles) {
Ok(config) => config,
Err(ConfigError(msg)) => {
eprintln!("Could not load config: {}", msg);
process::exit(1);
}
Err(e) => {
eprintln!("Could not load config: {}", e);
process::exit(1);
}
};
if args.flag_pager || config.display.use_pager {
configure_pager();
}
// Specify target OS
let os: OsType = match args.flag_os {
@ -344,24 +336,17 @@ fn main() {
}
// Update cache, pass through
if args.flag_update {
let cache_updated = if should_update_cache(&args, &config) {
update_cache(&cache, args.flag_quiet);
}
// Show config file and path, pass through
if args.flag_config_path {
show_config_path();
}
// Create a basic config and exit
if args.flag_seed_config {
create_config_and_exit();
}
true
} else {
false
};
// Render local file and exit
if let Some(ref file) = args.flag_render {
let path = PathBuf::from(file);
if let Err(msg) = print_page(&path, args.flag_markdown, enable_styles) {
if let Err(msg) = print_page(&path, args.flag_markdown, &config) {
eprintln!("{}", msg);
process::exit(1);
} else {
@ -371,8 +356,10 @@ fn main() {
// List cached commands and exit
if args.flag_list {
// Check cache for freshness
check_cache(&args);
if !cache_updated {
// Check cache for freshness
check_cache(&args);
}
// Get list of pages
let pages = cache.list_pages().unwrap_or_else(|e| {
@ -392,12 +379,15 @@ fn main() {
// Show command from cache
if let Some(ref command) = args.arg_command {
let command = command.join("-");
// Check cache for freshness
check_cache(&args);
if !cache_updated {
// Check cache for freshness
check_cache(&args);
}
// Search for command in cache
if let Some(path) = cache.find_page(&command) {
if let Err(msg) = print_page(&path, args.flag_markdown, enable_styles) {
if let Err(msg) = print_page(&path, args.flag_markdown, &config) {
eprintln!("{}", msg);
process::exit(1);
} else {

View file

@ -19,3 +19,7 @@ bold = false
[display]
use_pager = false
compact = false
[updates]
auto_update = false
auto_update_interval_hours = 720

View file

@ -2,13 +2,14 @@
extern crate assert_cmd;
extern crate escargot;
extern crate filetime;
extern crate predicates;
extern crate tempdir;
extern crate utime;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::process::Command;
use std::time::{Duration, SystemTime};
use assert_cmd::prelude::*;
use tempdir::TempDir;
@ -160,7 +161,11 @@ fn test_quiet_old_cache() {
.success()
.stdout(is_empty());
let _ = utime::set_file_times(testenv.cache_dir.path().join("tldr-master"), 1, 1).unwrap();
filetime::set_file_mtime(
testenv.cache_dir.path().join("tldr-master"),
filetime::FileTime::from_unix_time(1, 0),
)
.unwrap();
testenv
.command()
@ -354,3 +359,59 @@ fn test_list_flag_rendering() {
.success()
.stdout("bar\nbaz\nfoo\nqux\n");
}
#[test]
fn test_autoupdate_cache() {
let testenv = TestEnv::new();
// The first time, if automatic updates are disabled, the cache should not be found
testenv
.command()
.args(&["--list"])
.assert()
.failure()
.stderr(contains("Cache not found. Please run `tldr --update`."));
let config_file_path = testenv.config_dir.path().join("config.toml");
let cache_file_path = testenv.cache_dir.path().join("tldr-master");
// Activate automatic updates, set the auto-update interval to 24 hours
let mut config_file = File::create(&config_file_path).unwrap();
config_file
.write("[updates]\nauto_update = true\nauto_update_interval_hours = 24".as_bytes())
.unwrap();
config_file.flush().unwrap();
// Helper function that runs `tldr --list` and asserts that the cache is automatically updated
// or not, depending on the value of `expected`.
let check_cache_updated = |expected| {
let assert = testenv.command().args(&["--list"]).assert().success();
let pred = contains("Successfully updated cache");
if expected {
assert.stdout(pred)
} else {
assert.stdout(pred.not())
};
};
// The cache is updated the first time we run `tldr --list`
check_cache_updated(true);
// The cache is not updated with a subsequent call
check_cache_updated(false);
// We update the modification and access times such that they are about 23 hours from now.
// auto-update interval is 24 hours, the cache should not be updated
let new_mtime = SystemTime::now() - Duration::from_secs(82_800);
filetime::set_file_mtime(&cache_file_path, new_mtime.into()).unwrap();
check_cache_updated(false);
// We update the modification and access times such that they are about 25 hours from now.
// auto-update interval is 24 hours, the cache should be updated
let new_mtime = SystemTime::now() - Duration::from_secs(90_000);
filetime::set_file_mtime(&cache_file_path, new_mtime.into()).unwrap();
check_cache_updated(true);
// The cache is not updated with a subsequent call
check_cache_updated(false);
}