Compare commits

...

1 commit

Author SHA1 Message Date
Danilo Bargen
584885c78d Make network support optional 2019-01-01 23:23:53 +01:00
8 changed files with 576 additions and 321 deletions

View file

@ -18,9 +18,11 @@ jobs:
# Build # Build
- run: cargo build - run: cargo build
- run: cargo build --features logging - run: cargo build --features logging
- run: cargo build --no-default-features
# Run tests # Run tests
- run: cargo test - run: cargo test
- run: cargo test --no-default-features
- save_cache: - save_cache:
key: v1-cargo-cache-{{ arch }}-{{ .Branch }} key: v1-cargo-cache-{{ arch }}-{{ .Branch }}

651
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -28,17 +28,19 @@ time = "0.1.38"
toml = "0.4.6" toml = "0.4.6"
walkdir = "2.0.1" walkdir = "2.0.1"
xdg = "2.1.0" xdg = "2.1.0"
reqwest = "0.9.5" reqwest = { version = "0.9.5", optional = true }
[dev-dependencies] [dev-dependencies]
assert_cmd = "0.10" assert_cmd = "0.10"
escargot = "0.3" escargot = "0.4"
predicates = "1.0" predicates = "1.0"
tempdir = "^0.3" tempdir = "^0.3"
utime = "0.2.0" utime = "0.2.0"
[features] [features]
default = ["networking"]
logging = ["env_logger"] logging = ["env_logger"]
networking = ["reqwest"]
[profile.release] [profile.release]
lto = true lto = true

View file

@ -132,6 +132,7 @@ To enable the log output, set the `RUST_LOG` env variable:
$ export RUST_LOG=tldr=debug $ export RUST_LOG=tldr=debug
## Configuration ## Configuration
The tldr command can be customized with a config file called `config.toml`. The tldr command can be customized with a config file called `config.toml`.
@ -189,12 +190,25 @@ To run tests:
$ cargo test $ cargo test
(Note that integration tests are a bit slow, since they invoke `cargo build` in different configurations.)
To run lints: To run lints:
$ rustup component add clippy $ rustup component add clippy
$ cargo clean && cargo 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 ## License
Licensed under either of Licensed under either of

View file

@ -1,15 +1,19 @@
#[cfg(feature = "networking")]
use std::borrow::Cow;
use std::env; use std::env;
use std::fs; use std::fs;
use std::io::Read; 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; use std::os::unix::fs::MetadataExt;
#[cfg(feature = "networking")]
use reqwest::{Client, Proxy}; use reqwest::{Client, Proxy};
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use log::debug; use log::debug;
use tar::Archive; use tar::Archive;
#[cfg(feature = "networking")]
use time; use time;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use xdg::BaseDirectories; use xdg::BaseDirectories;
@ -17,19 +21,36 @@ use xdg::BaseDirectories;
use crate::error::TealdeerError::{self, CacheError, UpdateError}; use crate::error::TealdeerError::{self, CacheError, UpdateError};
use crate::types::OsType; 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)] #[derive(Debug)]
pub struct Cache { pub struct Cache {
url: String, /// The cache source. Either an URL or a file path.
source: Source,
/// The target OS type.
os: OsType, os: OsType,
} }
impl Cache { impl Cache {
pub fn new<S>(url: S, os: OsType) -> Self pub fn new<S>(source: S, os: OsType) -> Self
where where
S: Into<String>, S: Into<Source>,
{ {
Self { Self {
url: url.into(), source: source.into(),
os, os,
} }
} }
@ -60,8 +81,20 @@ impl Cache {
Ok(xdg_dirs.get_cache_home()) Ok(xdg_dirs.get_cache_home())
} }
/// Download the archive /// Load the archive from the file system.
fn download(&self) -> Result<Vec<u8>, TealdeerError> { 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(); let mut builder = Client::builder();
if let Ok(ref host) = env::var("HTTP_PROXY") { if let Ok(ref host) = env::var("HTTP_PROXY") {
if let Ok(proxy) = Proxy::http(host) { if let Ok(proxy) = Proxy::http(host) {
@ -74,13 +107,31 @@ impl Cache {
} }
} }
let client = builder.build().unwrap_or_else(|_| Client::new()); 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 mut buf: Vec<u8> = vec![];
let bytes_downloaded = resp.copy_to(&mut buf)?; let bytes_downloaded = resp.copy_to(&mut buf)?;
debug!("{} bytes downloaded", bytes_downloaded); debug!("{} bytes downloaded", bytes_downloaded);
Ok(buf) 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 /// Decompress and open the archive
fn decompress<R: Read>(&self, reader: R) -> Archive<GzDecoder<R>> { fn decompress<R: Read>(&self, reader: R) -> Archive<GzDecoder<R>> {
Archive::new(GzDecoder::new(reader)) Archive::new(GzDecoder::new(reader))
@ -88,8 +139,8 @@ impl Cache {
/// Update the pages cache. /// Update the pages cache.
pub fn update(&self) -> Result<(), TealdeerError> { pub fn update(&self) -> Result<(), TealdeerError> {
// First, download the compressed data // First, load the compressed data
let bytes: Vec<u8> = self.download()?; let bytes: Vec<u8> = self.load()?;
// Decompress the response body into an `Archive` // Decompress the response body into an `Archive`
let mut archive = self.decompress(&bytes[..]); let mut archive = self.decompress(&bytes[..]);
@ -118,7 +169,7 @@ impl Cache {
Ok(()) Ok(())
} }
#[cfg(unix)] #[cfg(all(unix, feature = "networking"))]
/// Return the number of seconds since the cache directory was last modified. /// Return the number of seconds since the cache directory was last modified.
pub fn last_update(&self) -> Option<i64> { pub fn last_update(&self) -> Option<i64> {
if let Ok(cache_dir) = self.get_cache_dir() { if let Ok(cache_dir) = self.get_cache_dir() {

View file

@ -1,4 +1,6 @@
use std::fmt; use std::fmt;
#[cfg(feature = "networking")]
use reqwest::Error as ReqwestError; use reqwest::Error as ReqwestError;
#[derive(Debug)] #[derive(Debug)]
@ -9,6 +11,7 @@ pub enum TealdeerError {
UpdateError(String), UpdateError(String),
} }
#[cfg(feature = "networking")]
impl From<ReqwestError> for TealdeerError { impl From<ReqwestError> for TealdeerError {
fn from(err: ReqwestError) -> Self { fn from(err: ReqwestError) -> Self {
TealdeerError::UpdateError(format!("HTTP error: {}", err.to_string())) TealdeerError::UpdateError(format!("HTTP error: {}", err.to_string()))

View file

@ -16,11 +16,14 @@
#[cfg(feature = "logging")] #[cfg(feature = "logging")]
extern crate env_logger; extern crate env_logger;
#[cfg(feature = "networking")]
use std::borrow::Cow;
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::BufReader;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
#[cfg(feature = "networking")]
use ansi_term::Color; use ansi_term::Color;
use docopt::Docopt; use docopt::Docopt;
use serde_derive::Deserialize; use serde_derive::Deserialize;
@ -32,7 +35,7 @@ mod formatter;
mod tokenizer; mod tokenizer;
mod types; mod types;
use crate::cache::Cache; use crate::cache::{Cache, Source};
use crate::config::{get_config_path, make_default_config, Config}; use crate::config::{get_config_path, make_default_config, Config};
use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError}; use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError};
use crate::formatter::print_lines; use crate::formatter::print_lines;
@ -42,6 +45,8 @@ use crate::types::OsType;
const NAME: &str = "tealdeer"; const NAME: &str = "tealdeer";
const VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = env!("CARGO_PKG_VERSION");
const USAGE: &str = " const USAGE: &str = "
tealdeer, a fast tldr implementation written in Rust.
Usage: Usage:
tldr [options] <command> tldr [options] <command>
@ -49,16 +54,17 @@ Usage:
Options: Options:
-h --help Show this screen -h --help Show this screen
-v --version Show version information -v --version Show version information
-l --list List all commands in the cache -l --list List all commands in the cache
-f --render <file> Render a specific markdown file -f --render <file> Render a specific markdown file
-o --os <type> Override the operating system [linux, osx, sunos] -o --os <type> Override the operating system [linux, osx, sunos]
-u --update Update the local cache -u --update Update the local cache from the network
-c --clear-cache Clear the local cache -U --update-from <src> Update the local cache from the specified URL or path
-q --quiet Suppress informational messages -c --clear-cache Clear the local cache
--config-path Show config file path -q --quiet Suppress informational messages
--seed-config Create a basic config --config-path Show config file path
--seed-config Create a basic config
Examples: Examples:
@ -70,11 +76,19 @@ To control the cache:
$ tldr --update $ tldr --update
$ tldr --clear-cache $ 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): To render a local file (for testing):
$ tldr --render /path/to/file.md $ tldr --render /path/to/file.md
"; ";
#[cfg(feature = "networking")]
const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz"; 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 const MAX_CACHE_AGE: i64 = 2_592_000; // 30 days
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -86,6 +100,7 @@ struct Args {
flag_render: Option<String>, flag_render: Option<String>,
flag_os: Option<OsType>, flag_os: Option<OsType>,
flag_update: bool, flag_update: bool,
flag_update_from: Option<String>,
flag_clear_cache: bool, flag_clear_cache: bool,
flag_quiet: bool, flag_quiet: bool,
flag_config_path: bool, flag_config_path: bool,
@ -119,6 +134,7 @@ fn print_page(path: &Path) -> Result<(), String> {
} }
/// Check the cache for freshness /// Check the cache for freshness
#[cfg(feature = "networking")]
fn check_cache(args: &Args, cache: &Cache) { fn check_cache(args: &Args, cache: &Cache) {
if !args.flag_update { if !args.flag_update {
match cache.last_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")] #[cfg(feature = "logging")]
fn init_log() { fn init_log() {
env_logger::init(); env_logger::init();
@ -176,6 +200,28 @@ fn get_os() -> OsType {
OsType::Other 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() { fn main() {
// Initialize logger // Initialize logger
init_log(); init_log();
@ -198,8 +244,15 @@ fn main() {
None => get_os(), 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 // Initialize cache
let cache = Cache::new(ARCHIVE_URL, os); let cache = Cache::new(cache_source, os);
// Clear cache, pass through // Clear cache, pass through
if args.flag_clear_cache { if args.flag_clear_cache {
@ -217,7 +270,7 @@ fn main() {
} }
// Update cache, pass through // Update cache, pass through
if args.flag_update { if args.flag_update || args.flag_update_from.is_some() {
cache.update().unwrap_or_else(|e| { cache.update().unwrap_or_else(|e| {
match e { match e {
CacheError(msg) | ConfigError(msg) | UpdateError(msg) => { CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {

View file

@ -1,11 +1,5 @@
//! Integration tests. //! Integration tests.
extern crate assert_cmd;
extern crate escargot;
extern crate predicates;
extern crate tempdir;
extern crate utime;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::process::Command; use std::process::Command;
@ -20,7 +14,6 @@ struct TestEnv {
pub config_dir: TempDir, pub config_dir: TempDir,
pub input_dir: TempDir, pub input_dir: TempDir,
pub default_features: bool, pub default_features: bool,
pub features: Vec<String>,
} }
impl TestEnv { impl TestEnv {
@ -30,7 +23,6 @@ impl TestEnv {
config_dir: TempDir::new(".tldr.test.config").unwrap(), config_dir: TempDir::new(".tldr.test.config").unwrap(),
input_dir: TempDir::new(".tldr.test.input").unwrap(), input_dir: TempDir::new(".tldr.test.input").unwrap(),
default_features: true, default_features: true,
features: vec![],
} }
} }
@ -40,12 +32,6 @@ impl TestEnv {
self 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. /// Return a new `Command` with env vars set.
fn command(&self) -> Command { fn command(&self) -> Command {
let mut build = escargot::CargoBuild::new() let mut build = escargot::CargoBuild::new()
@ -53,10 +39,9 @@ impl TestEnv {
.current_release() .current_release()
.current_target(); .current_target();
if !self.default_features { if !self.default_features {
build = build.arg("--no-default-features"); build = build
} .arg("--no-default-features")
if !self.features.is_empty() { .target_dir("target/no-default-features");
build = build.arg(&format!("--feature {}", self.features.join(",")));
} }
let run = build.run().unwrap(); let run = build.run().unwrap();
let mut cmd = run.command(); let mut cmd = run.command();
@ -257,3 +242,57 @@ fn test_correct_rendering_with_config() {
.success() .success()
.stdout(similar(expected)); .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"));
}