summaryrefslogtreecommitdiffstats
path: root/lib/src/client.rs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/src/client.rs')
-rw-r--r--lib/src/client.rs321
1 files changed, 321 insertions, 0 deletions
diff --git a/lib/src/client.rs b/lib/src/client.rs
new file mode 100644
index 0000000..da8ad94
--- /dev/null
+++ b/lib/src/client.rs
@@ -0,0 +1,321 @@
+use std::convert::TryFrom;
+
+use anyhow::Result;
+use ipfs::Cid;
+
+use crate::config::Config;
+use crate::ipfs_client::IpfsClient;
+use crate::types::Node;
+use crate::types::Payload;
+use crate::types::DateTime;
+
+pub struct Client {
+ pub(crate) ipfs: IpfsClient,
+ config: Config,
+}
+
+impl std::fmt::Debug for Client {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "Client {{ config: {:?} }}", self.config)
+ }
+}
+
+impl Client {
+ pub fn new(ipfs: IpfsClient, config: Config) -> Self {
+ Client {
+ ipfs,
+ config
+ }
+ }
+
+ pub async fn exit(self) -> Result<()> {
+ self.ipfs.exit_daemon().await;
+ Ok(())
+ }
+
+ pub async fn connect(&self, peer: ipfs::MultiaddrWithPeerId) -> Result<()> {
+ self.ipfs.connect(peer).await
+ }
+
+ pub async fn post_text_blob(&self, text: String) -> Result<Cid> {
+ self.ipfs
+ .put_dag(text.into())
+ .await
+ .map_err(anyhow::Error::from)
+ }
+
+ /// Post a text node
+ ///
+ /// Pass in the parents if there are any.
+ ///
+ /// # Note
+ ///
+ /// Does not verify if the `parents` cids point to actual Nodes!
+ ///
+ /// # Returns
+ ///
+ /// Returns the Cid of the newly created node, or an error
+ pub async fn post_text_node(&self, parents: Vec<Cid>, text: String) -> Result<Cid> {
+ self.post_text_node_with_datetime(parents, text, now()).await
+ }
+
+ // For testing
+ async fn post_text_node_with_datetime(&self, parents: Vec<Cid>, text: String, datetime: DateTime) -> Result<Cid> {
+ let text_blob_cid = self.post_text_blob(text).await?;
+
+ let payload = Payload::new(mime::TEXT_PLAIN_UTF_8.as_ref().to_string(), datetime, text_blob_cid);
+ let payload_cid = self.post_payload(payload).await?;
+
+ let node = Node::new(crate::consts::protocol_version(), parents, payload_cid);
+ self.post_node(node).await
+ }
+
+ async fn post_payload(&self, payload: Payload) -> Result<Cid> {
+ self.post(payload).await
+ }
+
+ async fn post_node(&self, node: Node) -> Result<Cid> {
+ self.post(node).await
+ }
+
+ async fn post<S: Into<ipfs::Ipld>>(&self, s: S) -> Result<Cid> {
+ let cid = self.ipfs.put_dag(s.into()).await?;
+ self.pin(&cid).await?;
+ Ok(cid)
+ }
+
+ async fn pin(&self, cid: &cid::Cid) -> Result<()> {
+ self.ipfs.insert_pin(cid, false).await.map_err(anyhow::Error::from)
+ }
+
+ pub async fn get_node(&self, cid: Cid) -> Result<Node> {
+ self.get::<Node>(cid).await
+ }
+
+ pub async fn get_payload(&self, cid: Cid) -> Result<Payload> {
+ self.get::<Payload>(cid).await
+ }
+
+ async fn get<D: TryFrom<ipfs::Ipld, Error = anyhow::Error>>(&self, cid: Cid) -> Result<D> {
+ let ipld = self.ipfs
+ .get_dag(ipfs::IpfsPath::new(ipfs::path::PathRoot::Ipld(cid)))
+ .await?;
+
+ D::try_from(ipld)
+ }
+
+ pub async fn get_content_text(&self, cid: Cid) -> Result<String> {
+ struct S(String);
+ impl TryFrom<ipfs::Ipld> for S {
+ type Error = anyhow::Error;
+ fn try_from(ipld: ipfs::Ipld) -> Result<Self> {
+ match ipld {
+ ipfs::Ipld::String(s) => Ok(S(s)),
+ _ => anyhow::bail!("Not a string"),
+ }
+ }
+ }
+
+ self.get::<S>(cid).await.map(|v| v.0)
+ }
+}
+
+fn now() -> DateTime {
+ chrono::offset::Utc::now().into()
+}
+
+#[cfg(test)]
+mod tests {
+ use std::convert::TryFrom;
+
+ use cid::Cid;
+
+ use crate::client::Client;
+ use crate::config::Config;
+ use crate::ipfs_client::IpfsClient;
+ use crate::types::DateTime;
+
+ fn mkdate(y: i32, m: u32, d: u32, hr: u32, min: u32, sec: u32) -> crate::types::DateTime {
+ use chrono::TimeZone;
+
+ chrono::prelude::Utc.ymd(y, m, d).and_hms(hr, min, sec).into()
+ }
+
+ async fn mk_ipfs() -> IpfsClient {
+ let mut opts = ipfs::IpfsOptions::inmemory_with_generated_keys();
+ opts.mdns = false;
+ let (ipfs, fut): (ipfs::Ipfs<ipfs::TestTypes>, _) = ipfs::UninitializedIpfs::new(opts).start().await.unwrap();
+ tokio::task::spawn(fut);
+ ipfs
+ }
+
+ #[tokio::test]
+ async fn test_post_text_blob() {
+ let _ = env_logger::try_init();
+ let ipfs = mk_ipfs().await;
+ let config = Config::default();
+ let client = Client::new(ipfs, config);
+
+ let cid = client.post_text_blob(String::from("text")).await;
+ assert!(cid.is_ok());
+ let cid = cid.unwrap();
+ let expected_cid = Cid::try_from("bafyreienmqqpz622nxgi7xvcx2jf7p3lyagqkwcj5ieil3mhx2zckfl35u").unwrap();
+ assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid);
+ }
+
+ #[tokio::test]
+ async fn test_post_text_node() {
+ let _ = env_logger::try_init();
+ let ipfs = mk_ipfs().await;
+ let config = Config::default();
+ let client = Client::new(ipfs, config);
+
+ let datetime = mkdate(2021, 11, 27, 12, 30, 0);
+
+ let cid = client.post_text_node_with_datetime(Vec::new(), String::from("text"), datetime).await;
+ assert!(cid.is_ok());
+ let cid = cid.unwrap();
+ let expected_cid = Cid::try_from("bafyreidem25zq66ktf42l2sjlxmbz5f66bedw3i4ippshhb3h7dxextfty").unwrap();
+ assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid);
+ }
+
+ #[tokio::test]
+ async fn test_post_text_node_roundtrip() {
+ let _ = env_logger::try_init();
+ let ipfs = mk_ipfs().await;
+ let config = Config::default();
+ let client = Client::new(ipfs, config);
+
+ let datetime = mkdate(2021, 11, 27, 12, 30, 0);
+
+ let text = "text-roundtrip";
+
+ let cid = client.post_text_node_with_datetime(Vec::new(), String::from(text), datetime.clone()).await;
+ assert!(cid.is_ok());
+ let cid = cid.unwrap();
+ let expected_cid = Cid::try_from("bafyreicwvx755ysg7zfflxhwhl4d6wuuxmmgfexjfvdhgndiugj37bsphq").unwrap();
+ assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid);
+
+ let node = client.get_node(cid).await;
+ assert!(node.is_ok());
+ let node = node.unwrap();
+
+ assert_eq!(*node.version(), crate::consts::protocol_version());
+ assert!(node.parents().is_empty());
+
+ let payload = client.get_payload(node.payload().clone()).await;
+ assert!(payload.is_ok());
+ let payload = payload.unwrap();
+
+ assert_eq!(payload.mime(), mime::TEXT_PLAIN_UTF_8.as_ref());
+ assert_eq!(payload.timestamp(), &datetime);
+
+ let content = client.get_content_text(payload.content().clone()).await;
+ assert!(content.is_ok(), "not ok: {:?}", content);
+ let content = content.unwrap();
+
+ assert_eq!(content, text);
+ }
+
+ #[tokio::test]
+ async fn test_post_text_chain() {
+ let _ = env_logger::try_init();
+ let ipfs = mk_ipfs().await;
+ let config = Config::default();
+ let client = Client::new(ipfs, config);
+
+ let chain_elements = vec![
+ (mkdate(2021, 11, 27, 12, 30, 0), "text1", "bafyreidaxkxog3bssyxxjxlsubgg6wauxbobp7gwyucs6gwzyrtsavb7yu"),
+ (mkdate(2021, 11, 27, 12, 31, 0), "text2", "bafyreifsgfl6tvcdn42kihjryg7fpjyjgi4v56bud2m2yniqjrrfn3ils4"),
+ (mkdate(2021, 11, 27, 12, 32, 0), "text3", "bafyreifnim44y6zfsc7jrf4xs3lbawlc4qqmk4tgmbqnflbggmvvuvul7a"),
+ ];
+
+ let mut prev: Option<ipfs::Cid> = None;
+ for (datetime, text, expected_cid) in chain_elements {
+ let parents = if let Some(previous) = prev.as_ref() {
+ vec![previous.clone()]
+ } else {
+ Vec::new()
+ };
+
+ let cid = client.post_text_node_with_datetime(parents, String::from(text), datetime.clone()).await;
+ assert!(cid.is_ok());
+ let cid = cid.unwrap();
+ let expected_cid = Cid::try_from(expected_cid).unwrap();
+ assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid);
+ prev = Some(cid);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_post_text_dag() {
+ let _ = env_logger::try_init();
+ let ipfs = mk_ipfs().await;
+ let config = Config::default();
+ let client = Client::new(ipfs, config);
+
+ async fn post_chain(client: &Client, chain_elements: &Vec<(DateTime, &str, &str)>) {
+ let mut prev: Option<ipfs::Cid> = None;
+ for (datetime, text, expected_cid) in chain_elements {
+ let parents = if let Some(previous) = prev.as_ref() {
+ vec![previous.clone()]
+ } else {
+ Vec::new()
+ };
+
+ let cid = client.post_text_node_with_datetime(parents, String::from(*text), datetime.clone()).await;
+ assert!(cid.is_ok());
+ let cid = cid.unwrap();
+ let expected_cid = Cid::try_from(*expected_cid).unwrap();
+ assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid);
+ prev = Some(cid);
+ }
+ }
+
+ // The following posts a DAG like this:
+ //
+ // * -- * -- * _
+ // \
+ // * -- * -- * -- *
+ // /
+ // * -
+
+ let chain_1_elements = vec![
+ (mkdate(2021, 11, 27, 12, 30, 0), "text1", "bafyreidaxkxog3bssyxxjxlsubgg6wauxbobp7gwyucs6gwzyrtsavb7yu"),
+ (mkdate(2021, 11, 27, 12, 31, 0), "text2", "bafyreifsgfl6tvcdn42kihjryg7fpjyjgi4v56bud2m2yniqjrrfn3ils4"),
+ (mkdate(2021, 11, 27, 12, 32, 0), "text3", "bafyreifnim44y6zfsc7jrf4xs3lbawlc4qqmk4tgmbqnflbggmvvuvul7a"),
+ ];
+
+ let chain_2_elements = vec![
+ (mkdate(2021, 11, 27, 12, 32, 0), "text4", "bafyreibfkbslobjydkl3tuiqms7dk243fendyqxi5myqkhxquz7arayuwe"),
+ (mkdate(2021, 11, 27, 12, 32, 0), "text5", "bafyreicpzj4lfhzsx5pacp2otk7qyyx353lwsvmkp4aplwgvyisg3y4mjm"),
+ ];
+
+ post_chain(&client, &chain_1_elements).await;
+ post_chain(&client, &chain_2_elements).await;
+
+ let cid = client.post_text_node_with_datetime(Vec::new(), String::from("text6"), mkdate(2021, 11, 27, 12, 32, 0)).await;
+ assert!(cid.is_ok());
+ let cid = cid.unwrap();
+ let expected_cid = Cid::try_from("bafyreifcpqvxzrgmcbdx5omysjfyupsvjxlrfzww5yh75ld7f7ox3vzno4").unwrap();
+ assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid);
+
+ let parents = vec![
+ // latest node in chain_1_elements
+ ipfs::Cid::try_from("bafyreifnim44y6zfsc7jrf4xs3lbawlc4qqmk4tgmbqnflbggmvvuvul7a").unwrap(),
+
+ // latest node in chain_2_elements
+ ipfs::Cid::try_from("bafyreicpzj4lfhzsx5pacp2otk7qyyx353lwsvmkp4aplwgvyisg3y4mjm").unwrap(),
+
+ // single node "text6"
+ cid
+ ];
+
+ let cid = client.post_text_node_with_datetime(parents, String::from("text7"), mkdate(2021, 11, 27, 12, 32, 0)).await;
+ assert!(cid.is_ok());
+ let cid = cid.unwrap();
+ let expected_cid = Cid::try_from("bafyreieuac7kvefkiu5ls7tqumaef5qiur7l3moa33ay2kaxxpjmfdjbey").unwrap();
+ assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid);
+ }
+
+}