Skip to content

Commit

Permalink
Merge branch '20230625-move_to_lib'
Browse files Browse the repository at this point in the history
  • Loading branch information
phyber committed Oct 10, 2023
2 parents 24b1a2c + 0926f26 commit 95f0480
Show file tree
Hide file tree
Showing 20 changed files with 614 additions and 190 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

## v0.13.0

- Bump MSRV to 1.65.0
- Bump MSRV to 1.68.2
- Bump dependencies
- Moved lots of functionality under `lib.rs`

## v0.12.0

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0"
readme = "README.md"
homepage = "https://github.com/phyber/hcdl"
repository = "https://github.com/phyber/hcdl"
rust-version = "1.65.0"
rust-version = "1.68.2"
resolver = "2"
categories = [
"command-line-utilities",
Expand All @@ -22,6 +22,14 @@ exclude = [
".github",
]

[[bin]]
name = "hcdl"
path = "src/main.rs"

[lib]
name = "hcdl"
path = "src/lib.rs"

[features]
default = [
"embed_gpg_key",
Expand All @@ -45,6 +53,7 @@ pgp = "0.10"
serde_json = "1.0"
sha2 = "0.10"
tempfile = "3.6"
thiserror = "1.0"

[dependencies.clap]
version = "4.3.8"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## Installation

`hcdl` is available for install from [crates.io] if you have a stable [Rust]
toolchain of at least v1.65.0 installed.
toolchain of at least v1.68.2 installed.

This can be done with the standard Cargo install command:

Expand Down
1 change: 1 addition & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ fn is_valid_install_dir(s: &str) -> Result<PathBuf, String> {
Ok(path.to_path_buf())
}

#[allow(clippy::too_many_lines)]
fn create_app() -> Command {
let app = Command::new(crate_name!())
.version(crate_version!())
Expand Down
165 changes: 114 additions & 51 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
// client: HTTP client and associated methods
#![forbid(unsafe_code)]
#![forbid(missing_docs)]
use crate::progressbar::ProgressBarBuilder;
use crate::shasums::Shasums;
use crate::signature::Signature;
use crate::tmpfile::TmpFile;
use anyhow::Result;
use crate::{
error::ClientError,
progressbar::ProgressBarBuilder,
shasums::Shasums,
signature::Signature,
tmpfile::TmpFile,
};
use bytes::Bytes;
use reqwest::Response;
use std::io::prelude::*;
use std::io::BufWriter;
use url::Url;

mod build;
mod config;
mod product_version;
/// Re-export of `build`.
pub mod build;

/// Re-export of `config`.
pub mod config;

/// Re-export of `product_version`.
pub mod product_version;

pub use config::ClientConfig;
use product_version::ProductVersion;

Expand All @@ -26,19 +34,28 @@ const USER_AGENT: &str = concat!(
env!("CARGO_PKG_VERSION"),
);

/// A [`Client`] for downloading [HashiCorp](https://www.hashicorp.com)
/// products.
#[derive(Debug)]
pub struct Client {
api_url: String,
client: reqwest::Client,
config: ClientConfig,
}

impl Client {
// Return a new reqwest client with our user-agent
pub fn new(config: ClientConfig) -> Result<Self> {
/// Creates a new [`Client`] with the given [`ClientConfig`].
///
/// # Errors
///
/// Errors if failing to build the [`reqwest::Client`].
pub fn new(config: ClientConfig) -> Result<Self, ClientError> {
// Get a new reqwest client with our user-agent
let client = reqwest::ClientBuilder::new()
.gzip(true)
.user_agent(USER_AGENT)
.build()?;
.build()
.map_err(|_err| ClientError::ClientBuilder)?;

let client = Self {
api_url: RELEASES_API.to_string(),
Expand All @@ -49,28 +66,49 @@ impl Client {
Ok(client)
}

// Version check the given product via the checkpoint API
pub async fn check_version(&self, product: &str) -> Result<ProductVersion> {
// We to_string here for the test scenario.
#![allow(clippy::to_string_in_format_args)]
/// Checks the current version of the given `product` against the
/// [HashiCorp](https://www.hashicorp.com) checkpoint API.
///
/// # Errors
///
/// Errors if:
/// - Failing to parse the created checkpoint URL
/// - Failing to get the product version
/// - Failing to create a [`crate::client::product_version::ProductVersion`]
pub async fn check_version(
&self,
product: &str,
) -> Result<ProductVersion, ClientError> {
let url = format!(
"{api}/{product}/latest",
api = self.api_url,
product = product,
);

let url = Url::parse(&url)?;
let url = Url::parse(&url)
.map_err(|_err| ClientError::Url("check_version"))?;

let resp = self.get(url)
.await?
.json::<ProductVersion>()
.await?;
.await
.map_err(|_err| ClientError::ProductVersion)?;

Ok(resp)
}

// Download from the given URL to the output file.
pub async fn download(&self, url: Url, tmpfile: &mut TmpFile) -> Result<()> {
/// Downloads content from the given `url` to `tmpfile`.
///
/// # Errors
///
/// Errors if:
/// - Failing to make a request to the given `url`
/// - Failing to download the content from the given `url`
/// - Failing to write the downloaded content to the `tmpfile`
pub async fn download(
&self,
url: Url,
tmpfile: &mut TmpFile,
) -> Result<(), ClientError> {
let file = tmpfile.handle()?;

// Start the GET and attempt to get a content-length
Expand All @@ -88,7 +126,11 @@ impl Client {
let mut writer = pb.wrap_write(writer);

// Start downloading chunks.
while let Some(chunk) = resp.chunk().await? {
while let Some(chunk) = resp
.chunk()
.await
.map_err(|_| ClientError::Chunk)?
{
// Write the chunk to the output file.
writer.write_all(&chunk)?;
}
Expand All @@ -98,82 +140,103 @@ impl Client {
Ok(())
}

// Perform an HTTP GET on the given URL
pub async fn get(&self, url: Url) -> Result<Response> {
/// Perform an HTTP GET on the given `url`.
async fn get(&self, url: Url) -> Result<Response, ClientError> {
let resp = self.client
.get(url)
.get(url.clone())
.send()
.await?;
.await
.map_err(|_err| ClientError::Get(url))?;

Ok(resp)
}

// Perform an HTTP GET on the given URL and return the result as Bytes
pub async fn get_bytes(&self, url: Url) -> Result<Bytes> {
/// Perform an HTTP GET on the given `url` and return the result as
/// [`Bytes`].
async fn get_bytes(&self, url: Url) -> Result<Bytes, ClientError> {
let resp = self.get(url)
.await?
.bytes()
.await?;
.await
.map_err(|_err| ClientError::GetBytes)?;

Ok(resp)
}

// Perform an HTTP GET on the given URL and return the result as a String
pub async fn get_text(&self, url: Url) -> Result<String> {
/// Perform an HTTP GET on the given `url` and return the result as a
/// `String`.
async fn get_text(&self, url: Url) -> Result<String, ClientError> {
let resp = self.get(url)
.await?
.text()
.await?;
.await
.map_err(|_err| ClientError::GetText)?;

Ok(resp)
}

// Get the shasums for the given product version and return a new Shasums.
/// Get the checksums for the given [`ProductVersion`] and return a new
/// [`Shasums`].
///
/// # Errors
///
/// Errors when failing to get the shasum file.
pub async fn get_shasums(
&self,
version: &ProductVersion,
) -> Result<Shasums> {
) -> Result<Shasums, ClientError> {
let url = version.shasums_url();
let shasums = self.get_text(url).await?;
let shasums = Shasums::new(shasums);

Ok(shasums)
}

// Get the signature for the given ProductVersion and return a new
// Signature.
/// Get the signature for the given [`ProductVersion`] and return a new
/// [`Signature`].
///
/// # Errors
///
/// Errors if:
/// - Failing to get the shasums signature
/// - Failing to create a [`Signature`]
pub async fn get_signature(
&self,
version: &ProductVersion,
) -> Result<Signature> {
) -> Result<Signature, ClientError> {
let url = version.shasums_signature_url();
let signature = self.get_bytes(url).await?;
let signature = Signature::new(signature)?;

Ok(signature)
}

// Get the ProductVersion for a given product and version.
/// Get the [`ProductVersion`] for a given `product` and `version`.
///
/// # Errors
///
/// Errors if:
/// - Failing to get the version from the remote server
/// - Failing to deserialize the obtained version into a
/// [`ProductVersion`]
pub async fn get_version(
&self,
product: &str,
version: &str,
) -> Result<ProductVersion> {
// We to_string here for the test scenario.
#![allow(clippy::to_string_in_format_args)]
) -> Result<ProductVersion, ClientError> {
let url = format!(
"{api}/{product}/{version}",
api = self.api_url,
product = product,
version = version,
);

let url = Url::parse(&url)?;
let url = Url::parse(&url)
.map_err(|_err| ClientError::Url("get_version"))?;

let resp = self.get(url)
.await?
.json::<ProductVersion>()
.await?;
.await
.map_err(|_err| ClientError::ProductVersion)?;

Ok(resp)
}
Expand Down Expand Up @@ -208,7 +271,7 @@ mod tests {

// Builds up the path to the test file
fn data_path(filename: &str) -> String {
format!("{}{}", TEST_DATA_DIR, filename)
format!("{TEST_DATA_DIR}{filename}")
}

fn read_file_bytes(path: &PathBuf) -> Bytes {
Expand Down Expand Up @@ -270,7 +333,7 @@ mod tests {
async fn test_get_bytes() {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let url = Url::parse(&format!("{}/test.txt", server_url)).unwrap();
let url = Url::parse(&format!("{server_url}/test.txt")).unwrap();
let expected = "Test text\n";
let data = data_path("test.txt");

Expand Down Expand Up @@ -304,22 +367,22 @@ mod tests {
name: "terraform".into(),
timestamp_created: DateTime::<Utc>::from_str("2020-05-27T16:55:35.000Z").unwrap(),
timestamp_updated: DateTime::<Utc>::from_str("2020-05-27T16:55:35.000Z").unwrap(),
url_shasums: Url::parse(&format!("{}/terraform/0.12.26/terraform_0.12.26_SHA256SUMS", server_url)).unwrap(),
url_shasums: Url::parse(&format!("{server_url}/terraform/0.12.26/terraform_0.12.26_SHA256SUMS")).unwrap(),
version: "0.12.26".into(),
builds: vec![
Build {
arch: "amd64".into(),
os: "freebsd".into(),
url: Url::parse(&format!("{}/terraform/0.12.26/terraform_0.12.26_freebsd_amd64.zip", server_url)).unwrap(),
url: Url::parse(&format!("{server_url}/terraform/0.12.26/terraform_0.12.26_freebsd_amd64.zip")).unwrap(),
},
Build {
arch: "amd64".into(),
os: "linux".into(),
url: Url::parse(&format!("{}/terraform/0.12.26/terraform_0.12.26_linux_amd64.zip", server_url)).unwrap(),
url: Url::parse(&format!("{server_url}/terraform/0.12.26/terraform_0.12.26_linux_amd64.zip")).unwrap(),
},
],
url_shasums_signatures: vec![
Url::parse(&format!("{}/terraform/0.12.26/terraform_0.12.26_SHA256SUMS.sig", server_url)).unwrap(),
Url::parse(&format!("{server_url}/terraform/0.12.26/terraform_0.12.26_SHA256SUMS.sig")).unwrap(),
],
};

Expand All @@ -344,7 +407,7 @@ mod tests {
async fn test_get_text() {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let url = Url::parse(&format!("{}/test.txt", server_url)).unwrap();
let url = Url::parse(&format!("{server_url}/test.txt")).unwrap();
let expected = Bytes::from("Test text\n");
let data = data_path("test.txt");

Expand Down
Loading

0 comments on commit 95f0480

Please sign in to comment.