Skip to content
1,085 changes: 70 additions & 1,015 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 3 additions & 6 deletions lib/gofer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ file = ["std"]
ftp = ["dep:percent-encoding", "dep:suppaftp"]
ftps = ["ftp", "suppaftp?/rustls"]
git = ["https"]
http = ["dep:reqwest", "reqwest?/blocking"]
https = ["http", "reqwest?/http2", "reqwest?/rustls-tls-native-roots"]
http = ["dep:ureq"]
https = ["http"]
ipfs = ["https"]
scp = ["dep:ssh2"]
stdin = ["std"]
Expand All @@ -60,10 +60,7 @@ miette = { version = "7.5", default-features = false, features = [
"derive",
], optional = true }
percent-encoding = { version = "2.3", default-features = false, optional = true }
reqwest = { version = "0.12", default-features = false, features = [
"charset",
"rustls-tls",
], optional = true }
ureq = { version = "3.0.12", features = ["charset"], optional = true }
ssh2 = { version = "0.9", default-features = false, features = [
"vendored-openssl",
], optional = true }
Expand Down
6 changes: 3 additions & 3 deletions lib/gofer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ pub enum Error {
url(docsrs),
)
)]
FailedHttpRequest(#[from] reqwest::Error),
FailedHttpRequest(#[from] ureq::Error),

#[cfg(feature = "git")]
#[error("invalid Git URL: {0}")]
Expand Down Expand Up @@ -213,10 +213,10 @@ impl TryInto<suppaftp::FtpError> for Error {
}

#[cfg(any(feature = "http", feature = "https"))]
impl TryInto<reqwest::Error> for Error {
impl TryInto<ureq::Error> for Error {
type Error = Error;

fn try_into(self) -> Result<reqwest::Error> {
fn try_into(self) -> Result<ureq::Error> {
match self {
Error::FailedHttpRequest(e) => Ok(e),
_ => Err(self),
Expand Down
55 changes: 55 additions & 0 deletions lib/gofer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#![deny(unsafe_code)]
//#![allow(unused)]

use std::collections::HashMap;
#[cfg(feature = "std")]
pub use std::io::{Cursor, Read};

Expand All @@ -34,3 +35,57 @@ pub use schemes::*;
#[doc = include_str!("../../../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;

#[derive(Clone, Debug)]
pub struct RequestConfig {
pub headers: HashMap<String, String>,
}

/// Configuration for HTTP requests.
///
/// This struct allows customizing HTTP requests with headers. It is used with protocol-specific
/// `open_with_config` functions to provide additional request parameters, such as custom HTTP headers.
///
/// # Examples
/// ```rust
/// use gofer::RequestConfig;
///
/// let config = RequestConfig::new()
/// .with_header("User-Agent", "gofer/1.0")
/// .with_header("Accept", "application/json");
/// assert_eq!(config.headers.len(), 2);
/// ```
impl RequestConfig {
/// Creates a new, empty `RequestConfig`.
///
/// # Returns
/// A `RequestConfig` instance with no headers set.
pub fn new() -> Self {
RequestConfig {
headers: HashMap::new(),
}
}

/// Adds a header to the configuration and returns the modified `RequestConfig`.
///
/// This method allows chaining to add multiple headers fluently.
///
/// # Arguments
/// * `key` - The header name (e.g., `"User-Agent"`).
/// * `value` - The header value (e.g., `"gofer/1.0"`).
///
/// # Returns
/// The modified `RequestConfig` with the new header added.
///
/// # Examples
/// ```rust
/// use gofer::RequestConfig;
///
/// let config = RequestConfig::new().with_header("Authorization", "Bearer token");
/// assert_eq!(config.headers.get("Authorization"), Some(&"Bearer token".to_string()));
/// ```
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
}
22 changes: 21 additions & 1 deletion lib/gofer/src/open.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This is free and unencumbered software released into the public domain.

use crate::{Error, Read, Result, Url, UrlScheme};
use crate::{Error, Read, RequestConfig, Result, Url, UrlScheme};

pub fn open(url: impl AsRef<str>) -> Result<Box<dyn Read>> {
let url = url.as_ref().parse::<Url>()?;
Expand Down Expand Up @@ -42,6 +42,26 @@ pub fn open(url: impl AsRef<str>) -> Result<Box<dyn Read>> {
}
}

pub fn open_with_config(url: impl AsRef<str>, config: RequestConfig) -> Result<Box<dyn Read>> {
Comment thread
artob marked this conversation as resolved.
let url = url.as_ref().parse::<Url>()?;

match url.scheme() {
#[cfg(feature = "git")]
UrlScheme::Git => crate::schemes::git::open_with_config(&url, config),

#[cfg(feature = "http")]
UrlScheme::Http => crate::schemes::http::open_with_config(&url, false, config),

#[cfg(feature = "https")]
UrlScheme::Https => crate::schemes::http::open_with_config(&url, true, config),

#[cfg(feature = "ipfs")]
UrlScheme::Ipfs => crate::schemes::ipfs::open_with_config(&url, config),

_ => Err(Error::UnknownScheme(url.scheme_str().to_string())),
}
}

#[cfg(feature = "std")]
pub fn open_buffered(url: impl AsRef<str>) -> Result<std::io::BufReader<Box<dyn Read>>> {
Ok(std::io::BufReader::new(open(url)?))
Expand Down
3 changes: 3 additions & 0 deletions lib/gofer/src/schemes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ pub mod scp;

#[cfg(feature = "stdin")]
pub mod stdin;

#[cfg(any(feature = "http", feature = "https", feature = "git", feature = "ipfs"))]
mod request;
86 changes: 63 additions & 23 deletions lib/gofer/src/schemes/git.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,78 @@
// This is free and unencumbered software released into the public domain.

use crate::{Error, Read, Result, Url};
use reqwest::{blocking::ClientBuilder, redirect};
use crate::{Error, Read, RequestConfig, Result, Url};
use crate::schemes::request;

static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

/// Downloads a file from a git repository.
/// Downloads a file from a git repository using a raw content URL.
///
/// Supports fetching files from:
/// - GitHub: `git://github.com/owner/repo/branch/...path`
/// - GitLab: `git://gitlab.com/owner/repo/branch/...path`
///
/// The function maps the provided git URL to a raw content URL and performs an HTTP GET request
/// to retrieve the file content as a readable stream.
///
/// # Arguments
/// * `url` - The git URL specifying the repository, branch, and file path.
///
/// Supports:
/// - GitHub: git://github.com/owner/repo/branch/...path
/// - GitLab: git://gitlab.com/owner/repo/branch/...path
/// # Returns
/// A `Result` containing a boxed readable stream (`Box<dyn Read>`) on success, or an `Error` on failure.
///
/// See: https://git-scm.com/docs/protocol-v2
/// See: https://git-scm.com/docs/gitweb
/// See: https://docs.github.com/en/repositories/working-with-files/using-files/getting-permanent-links-to-files
/// # References
/// - [Git Protocol v2](https://git-scm.com/docs/protocol-v2)
/// - [Git Web Interface](https://git-scm.com/docs/gitweb)
/// - [GitHub Permanent Links](https://docs.github.com/en/repositories/working-with-files/using-files/getting-permanent-links-to-files)
pub fn open<'a, 'b>(url: &'a Url<'b>) -> Result<Box<dyn Read>> {
// See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.ClientBuilder.html
let client = ClientBuilder::new()
.user_agent(USER_AGENT)
.redirect(redirect::Policy::default())
.https_only(true);

// See: https://docs.rs/ureq/3.0.12/ureq/struct.Agent.html
let agent = request::new_agent(true, None);
let url = map_git_url_to_raw_url(url.as_str())?;

// See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.Client.html#method.get
// See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.RequestBuilder.html
let response = client.build()?.get(url).send()?;
// See: https://docs.rs/ureq/3.0.12/ureq/struct.Agent.html#method.get
// See: https://docs.rs/ureq/3.0.12/ureq/struct.RequestBuilder.html#method.call
request::fetch(&agent, &url)
}

/// Downloads a file from a git repository using a raw content URL with custom request configuration.
///
/// Supports fetching files from:
/// - GitHub: `git://github.com/owner/repo/branch/...path`
/// - GitLab: `git://gitlab.com/owner/repo/branch/...path`
///
/// The function maps the provided git URL to a raw content URL and performs an HTTP GET request
/// with the specified configuration (e.g., custom headers) to retrieve the file content as a readable stream.
///
/// # Arguments
/// * `url` - The git URL specifying the repository, branch, and file path.
/// * `config` - Custom request configuration, such as HTTP headers.
///
/// # Returns
/// A `Result` containing a boxed readable stream (`Box<dyn Read>`) on success, or an `Error` on failure.
///
/// # References
/// - [Git Protocol v2](https://git-scm.com/docs/protocol-v2)
/// - [Git Web Interface](https://git-scm.com/docs/gitweb)
/// - [GitHub Permanent Links](https://docs.github.com/en/repositories/working-with-files/using-files/getting-permanent-links-to-files)
pub fn open_with_config<'a, 'b>(url: &'a Url<'b>, config: RequestConfig) -> Result<Box<dyn Read>> {
// See: https://docs.rs/ureq/3.0.12/ureq/struct.Agent.html
let agent = request::new_agent(true, None);
let url = map_git_url_to_raw_url(url.as_str())?;

Ok(Box::new(response))
// See: https://docs.rs/ureq/3.0.12/ureq/struct.Agent.html#method.get
// See: https://docs.rs/ureq/3.0.12/ureq/struct.RequestBuilder.html#method.call
request::fetch_with_config(&agent, &url, &config)
}

/// Maps a git URL to a raw content URL for supported git providers.
/// - GitHub: git://github.com/owner/repo/branch/...path -> https://raw.githubusercontent.com/owner/repo/refs/heads/branch/...path
/// - GitLab: git://gitlab.com/owner/repo/branch/...path -> https://gitlab.com/owner/repo/-/raw/branch/...path
///
/// Converts URLs like:
/// - GitHub: `git://github.com/owner/repo/branch/...path` → `https://raw.githubusercontent.com/owner/repo/refs/heads/branch/...path`
/// - GitLab: `git://gitlab.com/owner/repo/branch/...path` → `https://gitlab.com/owner/repo/-/raw/branch/...path`
///
/// # Arguments
/// * `url_str` - The git URL to map.
///
/// # Returns
/// A `Result` containing the raw content URL as a `String` on success, or an `Error` if the URL is invalid or the host is unsupported.
fn map_git_url_to_raw_url(url_str: &str) -> Result<String> {
let Some(path) = url_str.strip_prefix("git://") else {
return Err(Error::InvalidGitUrl(format!(
Expand Down
60 changes: 43 additions & 17 deletions lib/gofer/src/schemes/http.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,51 @@
// This is free and unencumbered software released into the public domain.

use crate::{Read, Result, Url};
use reqwest::{blocking::ClientBuilder, redirect};
use crate::{Read, RequestConfig, Result, Url};
use crate::schemes::request;

static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

/// See: https://en.wikipedia.org/wiki/HTTP
/// See: https://en.wikipedia.org/wiki/HTTPS
/// Performs an HTTP or HTTPS GET request to fetch a file from a URL.
///
/// # Arguments
/// * `url` - The URL to fetch (e.g., `http://example.com/file.txt` or `https://example.com/file.txt`).
/// * `secure` - Whether to use HTTPS (TLS) for the request.
///
/// # Returns
/// A `Result` containing a boxed readable stream (`Box<dyn Read>`) on success, or an `Error` on failure.
///
/// # References
/// - [HTTP Protocol](https://en.wikipedia.org/wiki/HTTP)
/// - [HTTPS Protocol](https://en.wikipedia.org/wiki/HTTPS)
pub fn open<'a, 'b>(url: &'a Url<'b>, secure: bool) -> Result<Box<dyn Read>> {
// See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.ClientBuilder.html
let mut client = ClientBuilder::new()
.user_agent(USER_AGENT)
.redirect(redirect::Policy::default());
// See: https://docs.rs/ureq/3.0.12/ureq/struct.Agent.html
let agent = request::new_agent(secure, None);

if secure {
client = client.https_only(true);
}
// See: https://docs.rs/ureq/3.0.12/ureq/struct.Agent.html#method.get
// See: https://docs.rs/ureq/3.0.12/ureq/struct.RequestBuilder.html#method.call
request::fetch(&agent, url.as_str())
}

// See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.Client.html#method.get
// See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.RequestBuilder.html
let response = client.build()?.get(url.as_str()).send()?;
/// Performs an HTTP or HTTPS GET request to fetch a file from a URL with custom request configuration.
///
/// # Arguments
/// * `url` - The URL to fetch (e.g., `http://example.com/file.txt` or `https://example.com/file.txt`).
/// * `secure` - Whether to use HTTPS (TLS) for the request.
/// * `config` - Custom request configuration, such as HTTP headers.
///
/// # Returns
/// A `Result` containing a boxed readable stream (`Box<dyn Read>`) on success, or an `Error` on failure.
///
/// # References
/// - [HTTP Protocol](https://en.wikipedia.org/wiki/HTTP)
/// - [HTTPS Protocol](https://en.wikipedia.org/wiki/HTTPS)
pub fn open_with_config<'a, 'b>(
url: &'a Url<'b>,
secure: bool,
config: RequestConfig
) -> Result<Box<dyn Read>> {
// See: https://docs.rs/ureq/3.0.12/ureq/struct.Agent.html
let agent = request::new_agent(secure, None);

Ok(Box::new(response))
// See: https://docs.rs/ureq/3.0.12/ureq/struct.Agent.html#method.get
// See: https://docs.rs/ureq/3.0.12/ureq/struct.RequestBuilder.html#method.call
request::fetch_with_config(&agent, url.as_str(), &config)
}
Loading