diff --git a/lib/gofer/Cargo.toml b/lib/gofer/Cargo.toml index 85f4d43..4a61967 100644 --- a/lib/gofer/Cargo.toml +++ b/lib/gofer/Cargo.toml @@ -23,7 +23,7 @@ std = [ "percent-encoding?/std", "thiserror/std", ] -all = ["data", "file", "ftp", "http", "https", "stdin"] +all = ["data", "file", "ftp", "http", "https", "ipfs", "stdin"] unstable = ["ftps", "git", "scp"] # Protocols: @@ -34,6 +34,7 @@ ftps = ["ftp", "suppaftp?/rustls"] git = ["dep:gix-protocol"] http = ["dep:reqwest", "reqwest?/blocking"] https = ["http", "reqwest?/default", "reqwest?/rustls-tls-native-roots"] +ipfs = ["dep:reqwest", "reqwest?/blocking"] scp = ["dep:ssh2"] stdin = ["std"] diff --git a/lib/gofer/examples/ipfs.rs b/lib/gofer/examples/ipfs.rs new file mode 100644 index 0000000..b6428f2 --- /dev/null +++ b/lib/gofer/examples/ipfs.rs @@ -0,0 +1,12 @@ +// This is free and unencumbered software released into the public domain. + +use std::{boxed::Box, error::Error, io::stdout, result::Result}; + +pub fn main() -> Result<(), Box> { + let mut output = stdout().lock(); + let mut input = + gofer::open("ipfs://bafybeifx7yeb55armcsxwwitkymga5xf53dxiarykms3ygqic223w5sk3m")?; + std::io::copy(&mut input, &mut output)?; + + Ok(()) +} diff --git a/lib/gofer/src/error.rs b/lib/gofer/src/error.rs index 0ea2f00..4e7b83d 100644 --- a/lib/gofer/src/error.rs +++ b/lib/gofer/src/error.rs @@ -112,13 +112,25 @@ pub enum Error { ) )] FailedHttpRequest(#[from] reqwest::Error), + + #[cfg(feature = "ipfs")] + #[error("invalid IPFS URL: {0}")] + #[cfg_attr( + feature = "miette", + diagnostic( + code(gofer::invalid_ipfs_url), + help("it seems that the URL is malformed in some way"), + url(docsrs), + ) + )] + InvalidIpfsUrl(String), } #[cfg(feature = "std")] -impl Into for Error { - fn into(self) -> std::io::Error { +impl From for std::io::Error { + fn from(value: Error) -> Self { use std::io::ErrorKind; - match self { + match value { Error::InvalidUrl(e) => std::io::Error::new(ErrorKind::InvalidInput, e), Error::UnknownScheme(s) => std::io::Error::new(ErrorKind::InvalidInput, s), @@ -142,6 +154,9 @@ impl Into for Error { #[cfg(any(feature = "http", feature = "https"))] Error::FailedHttpRequest(_e) => std::io::Error::from(ErrorKind::Other), // FIXME + + #[cfg(feature = "ipfs")] + Error::InvalidIpfsUrl(u) => std::io::Error::new(ErrorKind::InvalidInput, u.as_str()), } } } diff --git a/lib/gofer/src/open.rs b/lib/gofer/src/open.rs index e7a13cc..534a015 100644 --- a/lib/gofer/src/open.rs +++ b/lib/gofer/src/open.rs @@ -29,6 +29,9 @@ pub fn open(url: impl AsRef) -> Result> { #[cfg(feature = "https")] UrlScheme::Https => crate::schemes::http::open(&url, true), + #[cfg(feature = "ipfs")] + UrlScheme::Ipfs => crate::schemes::ipfs::open(&url), + #[cfg(feature = "scp")] UrlScheme::Scp => crate::schemes::scp::open(&url), diff --git a/lib/gofer/src/schemes.rs b/lib/gofer/src/schemes.rs index 2b6f631..1830e4d 100644 --- a/lib/gofer/src/schemes.rs +++ b/lib/gofer/src/schemes.rs @@ -15,6 +15,9 @@ pub mod git; #[cfg(any(feature = "http", feature = "https"))] pub mod http; +#[cfg(feature = "ipfs")] +pub mod ipfs; + #[cfg(feature = "scp")] pub mod scp; diff --git a/lib/gofer/src/schemes/ipfs.rs b/lib/gofer/src/schemes/ipfs.rs new file mode 100644 index 0000000..0f4936e --- /dev/null +++ b/lib/gofer/src/schemes/ipfs.rs @@ -0,0 +1,28 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{Error, Read, Result, Url}; +use reqwest::{blocking::ClientBuilder, redirect}; + +static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); +static GATEWAY: &str = "https://ipfs.io"; + +/// See: https://en.wikipedia.org/wiki/InterPlanetary_File_System +pub fn open<'a, 'b>(url: &'a Url<'b>) -> Result> { + // 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); + + let url = url + .as_str() + .strip_prefix("ipfs://") + .ok_or_else(|| Error::InvalidIpfsUrl(url.to_string())) + .map(|id| format!("{}/ipfs/{}", GATEWAY, id))?; + + // 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()?; + + Ok(Box::new(response)) +}