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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -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() {

View file

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

View file

@ -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) => {

View file

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