From 938f1902b37a9f21a2b0083b82b6117d43b4d3ab Mon Sep 17 00:00:00 2001 From: Samuel Sarle Date: Sat, 7 Jun 2025 13:33:09 +0300 Subject: [PATCH 1/3] Implement fetching `IPFS` resources --- lib/gofer/Cargo.toml | 19 ++++++++++--------- lib/gofer/examples/ipfs.rs | 12 ++++++++++++ lib/gofer/src/error.rs | 15 +++++++++++++++ lib/gofer/src/open.rs | 3 +++ lib/gofer/src/schemes.rs | 3 +++ lib/gofer/src/schemes/ipfs.rs | 28 ++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 lib/gofer/examples/ipfs.rs create mode 100644 lib/gofer/src/schemes/ipfs.rs diff --git a/lib/gofer/Cargo.toml b/lib/gofer/Cargo.toml index 85f4d43..f54e63b 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"] @@ -45,26 +46,26 @@ miette = ["dep:miette"] [dependencies] clap = { version = "4.5", default-features = false, features = [ - "derive", - "help", + "derive", + "help", ], optional = true } data-url = { version = "0.3", default-features = false, features = [ - "alloc", + "alloc", ], optional = true } dogma = { version = "0.1.5", default-features = false, features = [ - "iri", - "uri", + "iri", + "uri", ] } gix-protocol = { version = "0.49", default-features = false, features = [ - "blocking-client", + "blocking-client", ], optional = true } miette = { version = "7.5", default-features = false, features = [ - "derive", + "derive", ], optional = true } percent-encoding = { version = "2.3", default-features = false, optional = true } reqwest = { version = "0.12", default-features = false, optional = true } ssh2 = { version = "0.9", default-features = false, features = [ - "vendored-openssl", + "vendored-openssl", ], optional = true } suppaftp = { version = "6", default-features = false, optional = true } thiserror = { version = "2", default-features = false } 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..0e5fc57 100644 --- a/lib/gofer/src/error.rs +++ b/lib/gofer/src/error.rs @@ -112,6 +112,18 @@ 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")] @@ -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)) +} From 64055e053d9f00b665fd9c99ff1ea765a7a2ac2f Mon Sep 17 00:00:00 2001 From: Samuel Sarle Date: Sat, 7 Jun 2025 14:26:31 +0300 Subject: [PATCH 2/3] Replace `Into` by `From` Clippy message: ``` warning: an implementation of `From` is preferred since it gives you `Into<_>` for free where the reverse isn't true --> lib/gofer/src/error.rs:130:1 | 130 | impl Into for Error { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: `impl From for Foreign` is allowed by the orphan rules, for more information see https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#from_over_into = note: `#[warn(clippy::from_over_into)]` on by default help: replace the `Into` implementation with `From` | 130 ~ impl From for std::io::Error { 131 ~ fn from(val: Error) -> Self { 132 | use std::io::ErrorKind; 133 ~ match val { ``` --- lib/gofer/src/error.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gofer/src/error.rs b/lib/gofer/src/error.rs index 0e5fc57..4e7b83d 100644 --- a/lib/gofer/src/error.rs +++ b/lib/gofer/src/error.rs @@ -127,10 +127,10 @@ pub enum Error { } #[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), From e9f85ec47f83483e4a2dcf5603aee6b160acb45a Mon Sep 17 00:00:00 2001 From: Samuel Sarle Date: Sat, 7 Jun 2025 14:29:23 +0300 Subject: [PATCH 3/3] Revert whitespace changes --- lib/gofer/Cargo.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/gofer/Cargo.toml b/lib/gofer/Cargo.toml index f54e63b..4a61967 100644 --- a/lib/gofer/Cargo.toml +++ b/lib/gofer/Cargo.toml @@ -46,26 +46,26 @@ miette = ["dep:miette"] [dependencies] clap = { version = "4.5", default-features = false, features = [ - "derive", - "help", + "derive", + "help", ], optional = true } data-url = { version = "0.3", default-features = false, features = [ - "alloc", + "alloc", ], optional = true } dogma = { version = "0.1.5", default-features = false, features = [ - "iri", - "uri", + "iri", + "uri", ] } gix-protocol = { version = "0.49", default-features = false, features = [ - "blocking-client", + "blocking-client", ], optional = true } miette = { version = "7.5", default-features = false, features = [ - "derive", + "derive", ], optional = true } percent-encoding = { version = "2.3", default-features = false, optional = true } reqwest = { version = "0.12", default-features = false, optional = true } ssh2 = { version = "0.9", default-features = false, features = [ - "vendored-openssl", + "vendored-openssl", ], optional = true } suppaftp = { version = "6", default-features = false, optional = true } thiserror = { version = "2", default-features = false }