mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-09 09:49:10 +02:00
Make network support optional
This commit is contained in:
parent
89a2595495
commit
584885c78d
8 changed files with 576 additions and 321 deletions
|
|
@ -18,9 +18,11 @@ jobs:
|
|||
# Build
|
||||
- run: cargo build
|
||||
- run: cargo build --features logging
|
||||
- run: cargo build --no-default-features
|
||||
|
||||
# Run tests
|
||||
- run: cargo test
|
||||
- run: cargo test --no-default-features
|
||||
|
||||
- save_cache:
|
||||
key: v1-cargo-cache-{{ arch }}-{{ .Branch }}
|
||||
|
|
|
|||
651
Cargo.lock
generated
651
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -28,17 +28,19 @@ time = "0.1.38"
|
|||
toml = "0.4.6"
|
||||
walkdir = "2.0.1"
|
||||
xdg = "2.1.0"
|
||||
reqwest = "0.9.5"
|
||||
reqwest = { version = "0.9.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "0.10"
|
||||
escargot = "0.3"
|
||||
escargot = "0.4"
|
||||
predicates = "1.0"
|
||||
tempdir = "^0.3"
|
||||
utime = "0.2.0"
|
||||
|
||||
[features]
|
||||
default = ["networking"]
|
||||
logging = ["env_logger"]
|
||||
networking = ["reqwest"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -132,6 +132,7 @@ To enable the log output, set the `RUST_LOG` env variable:
|
|||
$ export RUST_LOG=tldr=debug
|
||||
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
The tldr command can be customized with a config file called `config.toml`.
|
||||
|
|
@ -189,12 +190,25 @@ To run tests:
|
|||
|
||||
$ cargo test
|
||||
|
||||
(Note that integration tests are a bit slow, since they invoke `cargo build` in different configurations.)
|
||||
|
||||
To run lints:
|
||||
|
||||
$ rustup component add clippy
|
||||
$ cargo clean && cargo clippy
|
||||
|
||||
|
||||
## Build Flags
|
||||
|
||||
tealdeer knows the following feature flags:
|
||||
|
||||
- `logging`: This enables logging support through [env_logger](https://docs.rs/env_logger/*/env_logger/)
|
||||
- `networking`: This enables support for updating the cache from the internet
|
||||
|
||||
By default, only the `networking` feature is enabled. To build tealdeer without
|
||||
networking support, use the `--no-default-features` Cargo flag.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
|
|
|||
75
src/cache.rs
75
src/cache.rs
|
|
@ -1,15 +1,19 @@
|
|||
#[cfg(feature = "networking")]
|
||||
use std::borrow::Cow;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(all(unix, feature = "networking"))]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
use reqwest::{Client, Proxy};
|
||||
use flate2::read::GzDecoder;
|
||||
use log::debug;
|
||||
use tar::Archive;
|
||||
#[cfg(feature = "networking")]
|
||||
use time;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use xdg::BaseDirectories;
|
||||
|
|
@ -17,19 +21,36 @@ use xdg::BaseDirectories;
|
|||
use crate::error::TealdeerError::{self, CacheError, UpdateError};
|
||||
use crate::types::OsType;
|
||||
|
||||
/// A cache update source.
|
||||
#[derive(Debug)]
|
||||
pub enum Source {
|
||||
/// Load the archive from the file system.
|
||||
File(PathBuf),
|
||||
|
||||
/// Load the archive from the network.
|
||||
#[cfg(feature = "networking")]
|
||||
Url(Cow<'static, str>),
|
||||
|
||||
/// No source is defined.
|
||||
#[cfg(not(feature = "networking"))]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
url: String,
|
||||
/// The cache source. Either an URL or a file path.
|
||||
source: Source,
|
||||
/// The target OS type.
|
||||
os: OsType,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn new<S>(url: S, os: OsType) -> Self
|
||||
pub fn new<S>(source: S, os: OsType) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
S: Into<Source>,
|
||||
{
|
||||
Self {
|
||||
url: url.into(),
|
||||
source: source.into(),
|
||||
os,
|
||||
}
|
||||
}
|
||||
|
|
@ -60,8 +81,20 @@ impl Cache {
|
|||
Ok(xdg_dirs.get_cache_home())
|
||||
}
|
||||
|
||||
/// Download the archive
|
||||
fn download(&self) -> Result<Vec<u8>, TealdeerError> {
|
||||
/// Load the archive from the file system.
|
||||
fn load_from_file(path: &Path) -> Result<Vec<u8>, TealdeerError> {
|
||||
let mut f = fs::File::open(path)
|
||||
.map_err(|e| UpdateError(format!("Could not open file: {}", e)))?;
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
let bytes_read = f.read_to_end(&mut buf)
|
||||
.map_err(|e| UpdateError(format!("Could not read file: {}", e)))?;
|
||||
debug!("{} bytes loaded from filesystem", bytes_read);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Load the archive from the network.
|
||||
#[cfg(feature = "networking")]
|
||||
fn load_from_network(url: &str) -> 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) {
|
||||
|
|
@ -74,13 +107,31 @@ impl Cache {
|
|||
}
|
||||
}
|
||||
let client = builder.build().unwrap_or_else(|_| Client::new());
|
||||
let mut resp = client.get(&self.url).send()?;
|
||||
let mut resp = client.get(url).send()?;
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
let bytes_downloaded = resp.copy_to(&mut buf)?;
|
||||
debug!("{} bytes downloaded", bytes_downloaded);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Download the archive from the network or load it from a file.
|
||||
#[cfg(feature = "networking")]
|
||||
fn load(&self) -> Result<Vec<u8>, TealdeerError> {
|
||||
match self.source {
|
||||
Source::File(ref path) => Self::load_from_file(path),
|
||||
Source::Url(ref url) => Self::load_from_network(url),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the archive from the file system.
|
||||
#[cfg(not(feature = "networking"))]
|
||||
fn load(&self) -> Result<Vec<u8>, TealdeerError> {
|
||||
match self.source {
|
||||
Source::File(ref path) => Self::load_from_file(path),
|
||||
Source::None => Err(TealdeerError::UpdateError("No update source defined".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decompress and open the archive
|
||||
fn decompress<R: Read>(&self, reader: R) -> Archive<GzDecoder<R>> {
|
||||
Archive::new(GzDecoder::new(reader))
|
||||
|
|
@ -88,8 +139,8 @@ impl Cache {
|
|||
|
||||
/// Update the pages cache.
|
||||
pub fn update(&self) -> Result<(), TealdeerError> {
|
||||
// First, download the compressed data
|
||||
let bytes: Vec<u8> = self.download()?;
|
||||
// First, load the compressed data
|
||||
let bytes: Vec<u8> = self.load()?;
|
||||
|
||||
// Decompress the response body into an `Archive`
|
||||
let mut archive = self.decompress(&bytes[..]);
|
||||
|
|
@ -118,7 +169,7 @@ impl Cache {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(all(unix, feature = "networking"))]
|
||||
/// Return the number of seconds since the cache directory was last modified.
|
||||
pub fn last_update(&self) -> Option<i64> {
|
||||
if let Ok(cache_dir) = self.get_cache_dir() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use std::fmt;
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
use reqwest::Error as ReqwestError;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -9,6 +11,7 @@ pub enum TealdeerError {
|
|||
UpdateError(String),
|
||||
}
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
impl From<ReqwestError> for TealdeerError {
|
||||
fn from(err: ReqwestError) -> Self {
|
||||
TealdeerError::UpdateError(format!("HTTP error: {}", err.to_string()))
|
||||
|
|
|
|||
79
src/main.rs
79
src/main.rs
|
|
@ -16,11 +16,14 @@
|
|||
#[cfg(feature = "logging")]
|
||||
extern crate env_logger;
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
use ansi_term::Color;
|
||||
use docopt::Docopt;
|
||||
use serde_derive::Deserialize;
|
||||
|
|
@ -32,7 +35,7 @@ mod formatter;
|
|||
mod tokenizer;
|
||||
mod types;
|
||||
|
||||
use crate::cache::Cache;
|
||||
use crate::cache::{Cache, Source};
|
||||
use crate::config::{get_config_path, make_default_config, Config};
|
||||
use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError};
|
||||
use crate::formatter::print_lines;
|
||||
|
|
@ -42,6 +45,8 @@ use crate::types::OsType;
|
|||
const NAME: &str = "tealdeer";
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const USAGE: &str = "
|
||||
tealdeer, a fast tldr implementation written in Rust.
|
||||
|
||||
Usage:
|
||||
|
||||
tldr [options] <command>
|
||||
|
|
@ -49,16 +54,17 @@ Usage:
|
|||
|
||||
Options:
|
||||
|
||||
-h --help Show this screen
|
||||
-v --version Show version information
|
||||
-l --list List all commands in the cache
|
||||
-f --render <file> Render a specific markdown file
|
||||
-o --os <type> Override the operating system [linux, osx, sunos]
|
||||
-u --update Update the local cache
|
||||
-c --clear-cache Clear the local cache
|
||||
-q --quiet Suppress informational messages
|
||||
--config-path Show config file path
|
||||
--seed-config Create a basic config
|
||||
-h --help Show this screen
|
||||
-v --version Show version information
|
||||
-l --list List all commands in the cache
|
||||
-f --render <file> Render a specific markdown file
|
||||
-o --os <type> Override the operating system [linux, osx, sunos]
|
||||
-u --update Update the local cache from the network
|
||||
-U --update-from <src> Update the local cache from the specified URL or path
|
||||
-c --clear-cache Clear the local cache
|
||||
-q --quiet Suppress informational messages
|
||||
--config-path Show config file path
|
||||
--seed-config Create a basic config
|
||||
|
||||
Examples:
|
||||
|
||||
|
|
@ -70,11 +76,19 @@ To control the cache:
|
|||
$ tldr --update
|
||||
$ tldr --clear-cache
|
||||
|
||||
You can also manually specify the archive to be used for updating the cache:
|
||||
|
||||
$ tldr --update-from https://github.com/tldr-pages/tldr/archive/master.tar.gz
|
||||
$ tldr --update-from master.tar.gz
|
||||
|
||||
To render a local file (for testing):
|
||||
|
||||
$ tldr --render /path/to/file.md
|
||||
";
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz";
|
||||
#[cfg(feature = "networking")]
|
||||
const MAX_CACHE_AGE: i64 = 2_592_000; // 30 days
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -86,6 +100,7 @@ struct Args {
|
|||
flag_render: Option<String>,
|
||||
flag_os: Option<OsType>,
|
||||
flag_update: bool,
|
||||
flag_update_from: Option<String>,
|
||||
flag_clear_cache: bool,
|
||||
flag_quiet: bool,
|
||||
flag_config_path: bool,
|
||||
|
|
@ -119,6 +134,7 @@ fn print_page(path: &Path) -> Result<(), String> {
|
|||
}
|
||||
|
||||
/// Check the cache for freshness
|
||||
#[cfg(feature = "networking")]
|
||||
fn check_cache(args: &Args, cache: &Cache) {
|
||||
if !args.flag_update {
|
||||
match cache.last_update() {
|
||||
|
|
@ -144,6 +160,14 @@ fn check_cache(args: &Args, cache: &Cache) {
|
|||
};
|
||||
}
|
||||
|
||||
/// Check the cache for freshness
|
||||
///
|
||||
/// No-op when networking support is disabled.
|
||||
#[cfg(not(feature = "networking"))]
|
||||
fn check_cache(_args: &Args, _cache: &Cache) {
|
||||
// No-op when no networking support is enabled.
|
||||
}
|
||||
|
||||
#[cfg(feature = "logging")]
|
||||
fn init_log() {
|
||||
env_logger::init();
|
||||
|
|
@ -176,6 +200,28 @@ fn get_os() -> OsType {
|
|||
OsType::Other
|
||||
}
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
fn get_url_source(source: &str) -> Source {
|
||||
Source::Url(Cow::Owned(source.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "networking"))]
|
||||
fn get_url_source(_source: &str) -> Source {
|
||||
eprintln!("tealdeer has been compiled without networking support,");
|
||||
eprintln!("cannot update the cache from a network URL.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
fn get_default_source() -> Source {
|
||||
Source::Url(Cow::Borrowed(ARCHIVE_URL))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "networking"))]
|
||||
fn get_default_source() -> Source {
|
||||
Source::None
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Initialize logger
|
||||
init_log();
|
||||
|
|
@ -198,8 +244,15 @@ fn main() {
|
|||
None => get_os(),
|
||||
};
|
||||
|
||||
// Validate cache source
|
||||
let cache_source: Source = match args.flag_update_from {
|
||||
Some(ref src) if src.starts_with("http") => get_url_source(src),
|
||||
Some(ref src) => Source::File(PathBuf::from(src)),
|
||||
None => get_default_source(),
|
||||
};
|
||||
|
||||
// Initialize cache
|
||||
let cache = Cache::new(ARCHIVE_URL, os);
|
||||
let cache = Cache::new(cache_source, os);
|
||||
|
||||
// Clear cache, pass through
|
||||
if args.flag_clear_cache {
|
||||
|
|
@ -217,7 +270,7 @@ fn main() {
|
|||
}
|
||||
|
||||
// Update cache, pass through
|
||||
if args.flag_update {
|
||||
if args.flag_update || args.flag_update_from.is_some() {
|
||||
cache.update().unwrap_or_else(|e| {
|
||||
match e {
|
||||
CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
|
||||
|
|
|
|||
75
tests/lib.rs
75
tests/lib.rs
|
|
@ -1,11 +1,5 @@
|
|||
//! Integration tests.
|
||||
|
||||
extern crate assert_cmd;
|
||||
extern crate escargot;
|
||||
extern crate predicates;
|
||||
extern crate tempdir;
|
||||
extern crate utime;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
|
@ -20,7 +14,6 @@ struct TestEnv {
|
|||
pub config_dir: TempDir,
|
||||
pub input_dir: TempDir,
|
||||
pub default_features: bool,
|
||||
pub features: Vec<String>,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
|
|
@ -30,7 +23,6 @@ impl TestEnv {
|
|||
config_dir: TempDir::new(".tldr.test.config").unwrap(),
|
||||
input_dir: TempDir::new(".tldr.test.input").unwrap(),
|
||||
default_features: true,
|
||||
features: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,12 +32,6 @@ impl TestEnv {
|
|||
self
|
||||
}
|
||||
|
||||
/// Add the specified feature.
|
||||
fn with_feature<S: Into<String>>(mut self, feature: S) -> Self {
|
||||
self.features.push(feature.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a new `Command` with env vars set.
|
||||
fn command(&self) -> Command {
|
||||
let mut build = escargot::CargoBuild::new()
|
||||
|
|
@ -53,10 +39,9 @@ impl TestEnv {
|
|||
.current_release()
|
||||
.current_target();
|
||||
if !self.default_features {
|
||||
build = build.arg("--no-default-features");
|
||||
}
|
||||
if !self.features.is_empty() {
|
||||
build = build.arg(&format!("--feature {}", self.features.join(",")));
|
||||
build = build
|
||||
.arg("--no-default-features")
|
||||
.target_dir("target/no-default-features");
|
||||
}
|
||||
let run = build.run().unwrap();
|
||||
let mut cmd = run.command();
|
||||
|
|
@ -257,3 +242,57 @@ fn test_correct_rendering_with_config() {
|
|||
.success()
|
||||
.stdout(similar(expected));
|
||||
}
|
||||
|
||||
/// Updating from a network URL should not be possible when networking support
|
||||
/// is not enabled.
|
||||
#[test]
|
||||
fn test_update_from_no_networking() {
|
||||
let testenv = TestEnv::new();
|
||||
testenv
|
||||
.no_default_features() // Disable networking
|
||||
.command()
|
||||
.args(&["--update-from", "https://github.com/tldr-pages/tldr/archive/master.tar.gz"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("compiled without networking support"))
|
||||
.stderr(contains("cannot update the cache from a network URL"));
|
||||
}
|
||||
|
||||
/// Updating from an invalid URL should result in an error message.
|
||||
#[test]
|
||||
fn test_update_from_invalid_url() {
|
||||
let testenv = TestEnv::new();
|
||||
testenv
|
||||
.command()
|
||||
.args(&["--update-from", "httpsss:github.com/tldr-pages/tldr/archive/master.tar.gz"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("Could not update cache: HTTP error"))
|
||||
.stderr(contains("URL scheme is not allowed"));
|
||||
}
|
||||
|
||||
/// Updating from the wrong (non-gzip-archive) URL should result in an error.
|
||||
#[test]
|
||||
fn test_update_from_wrong_url() {
|
||||
let testenv = TestEnv::new();
|
||||
testenv
|
||||
.command()
|
||||
.args(&["--update-from", "https://tldr.sh/"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("Could not update cache: Could not unpack compressed data"));
|
||||
}
|
||||
|
||||
/// When a path is specified that does not exist, an error message should be shown.
|
||||
#[test]
|
||||
fn test_update_from_missing_path() {
|
||||
let testenv = TestEnv::new();
|
||||
testenv
|
||||
.no_default_features()
|
||||
.command()
|
||||
.args(&["--update-from", "ajsdfasjdkfljasdf"]) // Invalid path
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("Could not update cache: Could not open file:"))
|
||||
.stderr(contains("No such file or directory"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue