summaryrefslogtreecommitdiffstats
path: root/lib/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/src')
-rw-r--r--lib/src/cid.rs54
-rw-r--r--lib/src/client.rs321
-rw-r--r--lib/src/config.rs10
-rw-r--r--lib/src/consts.rs4
-rw-r--r--lib/src/ipfs_client.rs5
-rw-r--r--lib/src/lib.rs6
-rw-r--r--lib/src/profile/mod.rs181
-rw-r--r--lib/src/profile/state.rs129
-rw-r--r--lib/src/types/datetime.rs35
-rw-r--r--lib/src/types/mod.rs8
-rw-r--r--lib/src/types/node.rs88
-rw-r--r--lib/src/types/payload.rs72
12 files changed, 913 insertions, 0 deletions
diff --git a/lib/src/cid.rs b/lib/src/cid.rs
new file mode 100644
index 0000000..2957cc5
--- /dev/null
+++ b/lib/src/cid.rs
@@ -0,0 +1,54 @@
+use anyhow::Result;
+
+/// Our own CID type
+///
+/// Right now the ipfs_api crate does not use a CID type in its interface... hence we would need to
+/// convert back-and-forth between String and cid::Cid,... but that's tedious.
+///
+/// Hence we just create our own "Cid type" and use that as long as the crate API is stringly
+/// typed.
+#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
+#[serde(transparent)]
+pub struct Cid(String);
+
+impl AsRef<str> for Cid {
+ fn as_ref(&self) -> &str {
+ self.0.as_ref()
+ }
+}
+
+pub trait TryToCid {
+ fn try_to_cid(self) -> Result<Cid>;
+}
+
+impl daglib::NodeId for Cid {
+}
+
+/// Helper function that can be tested
+///
+/// Converts a String to a Cid
+#[cfg(not(test))]
+fn string_to_cid(s: String) -> Result<Cid> {
+ string_to_cid_impl(s)
+}
+
+#[cfg(test)]
+pub fn string_to_cid(s: String) -> Result<Cid> {
+ string_to_cid_impl(s)
+}
+
+fn string_to_cid_impl(s: String) -> Result<Cid> {
+ Ok(Cid(s))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_string_to_cid() {
+ let s = String::from("QmY2T5EfgLn8qWCt8eus6VX1gJuAp1nmUSdmoehgMxznAf");
+ let r = string_to_cid(s);
+ assert!(r.is_ok(), "Not OK = {:?}", r);
+ }
+}
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);
+ }
+
+}
diff --git a/lib/src/config.rs b/lib/src/config.rs
new file mode 100644
index 0000000..0d25e78
--- /dev/null
+++ b/lib/src/config.rs
@@ -0,0 +1,10 @@
+#[derive(Debug)]
+pub struct Config {
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Config { }
+ }
+}
+
diff --git a/lib/src/consts.rs b/lib/src/consts.rs
new file mode 100644
index 0000000..79ff2f5
--- /dev/null
+++ b/lib/src/consts.rs
@@ -0,0 +1,4 @@
+pub fn protocol_version() -> String {
+ String::from("1")
+}
+
diff --git a/lib/src/ipfs_client.rs b/lib/src/ipfs_client.rs
new file mode 100644
index 0000000..964b22c
--- /dev/null
+++ b/lib/src/ipfs_client.rs
@@ -0,0 +1,5 @@
+#[cfg(not(test))]
+pub type IpfsClient = ipfs::Ipfs<ipfs::Types>;
+
+#[cfg(test)]
+pub type IpfsClient = ipfs::Ipfs<ipfs::TestTypes>;
diff --git a/lib/src/lib.rs b/lib/src/lib.rs
new file mode 100644
index 0000000..759ea14
--- /dev/null
+++ b/lib/src/lib.rs
@@ -0,0 +1,6 @@
+pub mod client;
+pub mod config;
+pub mod consts;
+pub mod ipfs_client;
+pub mod profile;
+pub mod types;
diff --git a/lib/src/profile/mod.rs b/lib/src/profile/mod.rs
new file mode 100644
index 0000000..db3c593
--- /dev/null
+++ b/lib/src/profile/mod.rs
@@ -0,0 +1,181 @@
+use std::path::PathBuf;
+use std::convert::TryInto;
+
+use anyhow::Context;
+use anyhow::Result;
+
+use crate::client::Client;
+use crate::config::Config;
+use crate::ipfs_client::IpfsClient;
+
+mod state;
+use state::*;
+
+#[derive(Debug, getset::Getters)]
+pub struct Profile {
+ state: ProfileState,
+
+ #[getset(get = "pub")]
+ client: Client,
+}
+
+impl Profile {
+ pub async fn create(state_dir: &StateDir, name: &str, config: Config) -> Result<Self> {
+ let bootstrap = vec![]; // TODO
+ let mdns = true; // TODO
+ let keypair = ipfs::Keypair::generate_ed25519();
+
+ let options = ipfs::IpfsOptions {
+ ipfs_path: Self::ipfs_path(state_dir).await?,
+ keypair,
+ bootstrap,
+ mdns,
+ kad_protocol: None,
+ listening_addrs: vec![],
+ span: Some(tracing::trace_span!("distrox-ipfs")),
+ };
+
+ let keypair = options.keypair.clone();
+ let (ipfs, fut): (ipfs::Ipfs<_>, _) = ipfs::UninitializedIpfs::<_>::new(options)
+ .start()
+ .await?;
+ tokio::task::spawn(fut);
+ Self::new(ipfs, config, name.to_string(), keypair).await
+ }
+
+ pub async fn new_inmemory(config: Config, name: &str) -> Result<Self> {
+ let mut opts = ipfs::IpfsOptions::inmemory_with_generated_keys();
+ opts.mdns = true;
+ let keypair = opts.keypair.clone();
+ let (ipfs, fut): (ipfs::Ipfs<_>, _) = ipfs::UninitializedIpfs::<_>::new(opts).start().await.unwrap();
+ tokio::task::spawn(fut);
+ Self::new(ipfs, config, format!("inmemory-{}", name), keypair).await
+ }
+
+ async fn new(ipfs: IpfsClient, config: Config, profile_name: String, keypair: libp2p::identity::Keypair) -> Result<Self> {
+ let client = Client::new(ipfs, config);
+ let state = ProfileState::new(profile_name, keypair);
+ Ok(Profile { state, client })
+ }
+
+ pub fn head(&self) -> Option<&cid::Cid> {
+ self.state.profile_head().as_ref()
+ }
+
+ pub async fn connect(&self, peer: ipfs::MultiaddrWithPeerId) -> Result<()> {
+ self.client.connect(peer).await
+ }
+
+ async fn ipfs_path(state_dir: &StateDir) -> Result<PathBuf> {
+ let path = state_dir.ipfs();
+ tokio::fs::create_dir_all(&path).await?;
+ Ok(path)
+ }
+
+ pub fn config_path(name: &str) -> String {
+ format!("distrox-{}", name)
+ }
+
+ pub fn config_file_path(name: &str) -> Result<PathBuf> {
+ xdg::BaseDirectories::with_prefix("distrox")
+ .map_err(anyhow::Error::from)
+ .and_then(|dirs| {
+ let name = Self::config_path(name);
+ dirs.place_config_file(name)
+ .map_err(anyhow::Error::from)
+ })
+ }
+
+ pub fn state_dir_path(name: &str) -> Result<StateDir> {
+ log::debug!("Getting state directory path");
+ xdg::BaseDirectories::with_prefix("distrox")
+ .context("Fetching 'distrox' XDG base directory")
+ .map_err(anyhow::Error::from)
+ .and_then(|dirs| {
+ dirs.create_state_directory(name)
+ .map(StateDir::from)
+ .with_context(|| format!("Creating 'distrox' XDG state directory for '{}'", name))
+ .map_err(anyhow::Error::from)
+ })
+ }
+
+ pub async fn save(&self) -> Result<()> {
+ let state_dir_path = Self::state_dir_path(self.state.profile_name())?;
+ log::trace!("Saving to {:?}", state_dir_path.display());
+ ProfileStateSaveable::new(&self.state)
+ .context("Serializing profile state")?
+ .save_to_disk(&state_dir_path)
+ .await
+ .context("Saving state to disk")
+ .map_err(anyhow::Error::from)
+ }
+
+ pub async fn load(config: Config, name: &str) -> Result<Self> {
+ let state_dir_path = Self::state_dir_path(name)?;
+ log::trace!("state_dir_path = {:?}", state_dir_path.display());
+ let state: ProfileState = ProfileStateSaveable::load_from_disk(&state_dir_path)
+ .await?
+ .try_into()
+ .context("Parsing profile state")?;
+ log::debug!("Loading state finished");
+
+ let bootstrap = vec![]; // TODO
+ let mdns = true; // TODO
+ let keypair = state.keypair().clone();
+
+ log::debug!("Configuring IPFS backend");
+ let options = ipfs::IpfsOptions {
+ ipfs_path: Self::ipfs_path(&state_dir_path).await?,
+ keypair,
+ bootstrap,
+ mdns,
+ kad_protocol: None,
+ listening_addrs: vec![],
+ span: Some(tracing::trace_span!("distrox-ipfs")),
+ };
+
+ log::debug!("Starting IPFS backend");
+ let (ipfs, fut): (ipfs::Ipfs<_>, _) = ipfs::UninitializedIpfs::<_>::new(options)
+ .start()
+ .await?;
+ tokio::task::spawn(fut);
+
+ log::debug!("Profile loading finished");
+ Ok(Profile {
+ state,
+ client: Client::new(ipfs, config),
+ })
+ }
+
+ pub async fn exit(self) -> Result<()> {
+ self.client.exit().await
+ }
+
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::convert::TryFrom;
+ use crate::config::Config;
+
+ #[tokio::test]
+ async fn test_create_profile() {
+ let _ = env_logger::try_init();
+ let profile = Profile::new_inmemory(Config::default(), "test-create-profile").await;
+ assert!(profile.is_ok());
+ let exit = profile.unwrap().exit().await;
+ assert!(exit.is_ok(), "Not cleanly exited: {:?}", exit);
+ }
+
+ #[tokio::test]
+ async fn test_create_profile_and_helloworld() {
+ let _ = env_logger::try_init();
+ let profile = Profile::new_inmemory(Config::default(), "test-create-profile-and-helloworld").await;
+ assert!(profile.is_ok());
+ let profile = profile.unwrap();
+ assert!(profile.head().is_none());
+ }
+
+}
diff --git a/lib/src/profile/state.rs b/lib/src/profile/state.rs
new file mode 100644
index 0000000..4075b52
--- /dev/null
+++ b/lib/src/profile/state.rs
@@ -0,0 +1,129 @@
+use std::path::PathBuf;
+use std::convert::TryFrom;
+use std::convert::TryInto;
+
+use anyhow::Context;
+use anyhow::Result;
+use tokio::io::AsyncWriteExt;
+
+#[derive(Debug)]
+pub struct StateDir(PathBuf);
+
+impl StateDir {
+ pub fn ipfs(&self) -> PathBuf {
+ self.0.join("ipfs")
+ }
+
+ pub fn profile_state(&self) -> PathBuf {
+ self.0.join("profile_state")
+ }
+
+ pub fn display(&self) -> std::path::Display {
+ self.0.display()
+ }
+}
+
+impl From<PathBuf> for StateDir {
+ fn from(p: PathBuf) -> Self {
+ Self(p)
+ }
+}
+
+#[derive(getset::Getters)]
+pub struct ProfileState {
+ #[getset(get = "pub")]
+ profile_head: Option<cid::Cid>,
+
+ #[getset(get = "pub")]
+ profile_name: String,
+
+ #[getset(get = "pub")]
+ keypair: libp2p::identity::Keypair,
+}
+
+impl ProfileState {
+ pub(super) fn new(profile_name: String, keypair: libp2p::identity::Keypair) -> Self {
+ Self {
+ profile_head: None,
+ profile_name,
+ keypair
+ }
+ }
+}
+
+impl std::fmt::Debug for ProfileState {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "ProfileState {{ name = {}, head = {:?} }}", self.profile_name, self.profile_head)
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, getset::Getters)]
+pub(super) struct ProfileStateSaveable {
+ profile_head: Option<Vec<u8>>,
+ profile_name: String,
+ keypair: Vec<u8>,
+}
+
+impl ProfileStateSaveable {
+ pub(super) fn new(s: &ProfileState) -> Result<Self> {
+ Ok(Self {
+ profile_head: s.profile_head.clone().map(|v| v.to_bytes()),
+ profile_name: s.profile_name.clone(),
+ keypair: match s.keypair {
+ libp2p::identity::Keypair::Ed25519(ref kp) => Vec::from(kp.encode()),
+ _ => anyhow::bail!("Only keypair type ed25519 supported"),
+ }
+ })
+ }
+
+ pub async fn save_to_disk(&self, state_dir_path: &StateDir) -> Result<()> {
+ let state_s = serde_json::to_string(&self).context("Serializing state")?;
+ tokio::fs::OpenOptions::new()
+ .create_new(false) // do not _always_ create a new file
+ .create(true)
+ .truncate(true)
+ .write(true)
+ .open(&state_dir_path.profile_state())
+ .await
+ .with_context(|| format!("Opening {}", state_dir_path.profile_state().display()))?
+ .write_all(state_s.as_bytes())
+ .await
+ .map(|_| ())
+ .with_context(|| format!("Writing to {}", state_dir_path.profile_state().display()))
+ .map_err(anyhow::Error::from)
+ }
+
+ pub async fn load_from_disk(state_dir_path: &StateDir) -> Result<Self> {
+ log::trace!("Loading from disk: {:?}", state_dir_path.profile_state().display());
+ let reader = tokio::fs::OpenOptions::new()
+ .read(true)
+ .open(&state_dir_path.profile_state())
+ .await
+ .context("Opening state file")?
+ .into_std()
+ .await;
+
+ log::trace!("Parsing state file");
+ serde_json::from_reader(reader)
+ .context("Parsing state file")
+ .map_err(anyhow::Error::from)
+ }
+
+}
+
+impl TryInto<ProfileState> for ProfileStateSaveable {
+ type Error = anyhow::Error;
+
+ fn try_into(mut self) -> Result<ProfileState> {
+ Ok(ProfileState {
+ profile_head: self.profile_head.map(|h| cid::Cid::try_from(h)).transpose()?,
+ profile_name: self.profile_name,
+ keypair: {
+ let kp = libp2p::identity::ed25519::Keypair::decode(&mut self.keypair)?;
+ libp2p::identity::Keypair::Ed25519(kp)
+ },
+ })
+ }
+}
+
+
diff --git a/lib/src/types/datetime.rs b/lib/src/types/datetime.rs
new file mode 100644
index 0000000..00d739a
--- /dev/null
+++ b/lib/src/types/datetime.rs
@@ -0,0 +1,35 @@
+use std::convert::TryFrom;
+use anyhow::Error;
+use anyhow::Result;
+
+#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
+#[serde(transparent)]
+pub struct DateTime(chrono::DateTime<chrono::Utc>);
+
+impl Into<ipfs::Ipld> for DateTime {
+ fn into(self) -> ipfs::Ipld {
+ ipfs::Ipld::String(self.0.to_rfc3339())
+ }
+}
+
+impl TryFrom<ipfs::Ipld> for DateTime {
+ type Error = Error;
+
+ fn try_from(ipld: ipfs::Ipld) -> Result<DateTime> {
+ match ipld {
+ ipfs::Ipld::String(s) => chrono::DateTime::parse_from_rfc3339(&s)
+ .map(|dt| dt.with_timezone(&chrono::Utc))
+ .map(DateTime)
+ .map_err(Error::from),
+ _ => anyhow::bail!("Expected string for timestamp"),
+ }
+ }
+}
+
+
+impl From<chrono::DateTime<chrono::Utc>> for DateTime {
+ fn from(dt: chrono::DateTime<chrono::Utc>) -> Self {
+ DateTime(dt)
+ }
+}
+
diff --git a/lib/src/types/mod.rs b/lib/src/types/mod.rs
new file mode 100644
index 0000000..7382f16
--- /dev/null
+++ b/lib/src/types/mod.rs
@@ -0,0 +1,8 @@
+mod node;
+pub use node::*;
+
+mod datetime;
+pub use datetime::*;
+
+mod payload;
+pub use payload::*;
diff --git a/lib/src/types/node.rs b/lib/src/types/node.rs
new file mode 100644
index 0000000..eb5679b
--- /dev/null
+++ b/lib/src/types/node.rs
@@ -0,0 +1,88 @@
+use anyhow::Result;
+
+use std::convert::TryFrom;
+
+#[derive(Debug, Eq, PartialEq, getset::Getters)]
+pub struct Node {
+ /// Version
+ #[getset(get = "pub")]
+ version: String,
+
+ /// Parent Nodes, identified by cid
+ parents: Vec<ipfs::Cid>,
+
+ /// The actual payload of the node, which is stored in another document identified by this cid
+ payload: ipfs::Cid,
+}
+
+impl Into<ipfs::Ipld> for Node {
+ fn into(self) -> ipfs::Ipld {
+ let mut map = std::collections::BTreeMap::new();
+ map.insert(String::from("version"), ipfs::Ipld::String(self.version));
+ map.insert(String::from("parents"), ipfs::Ipld::List(self.parents.into_iter().map(ipfs::Ipld::Link).collect()));
+ map.insert(String::from("payload"), ipfs::Ipld::Link(self.payload));
+ ipfs::Ipld::Map(map)
+ }
+}
+
+impl TryFrom<ipfs::Ipld> for Node {
+ type Error = anyhow::Error;
+
+ fn try_from(ipld: ipfs::Ipld) -> Result<Self> {
+ let missing_field = |name: &'static str| move || anyhow::anyhow!("Missing field {}", name);
+ let field_wrong_type = |name: &str, expty: &str| anyhow::bail!("Field {} has wrong type, expected {}", name, expty);
+ match ipld {
+ ipfs::Ipld::Map(map) => {
+ let version = match map.get("version").ok_or_else(missing_field("version"))? {
+ ipfs::Ipld::String(s) => s.to_string(),
+ _ => return field_wrong_type("version", "String")
+ };
+
+ let parents = match map.get("parents").ok_or_else(missing_field("parents"))? {
+ ipfs::Ipld::List(s) => {
+ s.into_iter()
+ .map(|parent| -> Result<ipfs::Cid> {
+ match parent {
+ ipfs::Ipld::Link(cid) => Ok(cid.clone()),
+ _ => anyhow::bail!("Field in parents has wrong type, expected Link"),
+ }
+ })
+ .collect::<Result<Vec<ipfs::Cid>>>()?
+ },
+ _ => return field_wrong_type("parents", "Vec<Link>")
+ };
+
+ let payload = match map.get("payload").ok_or_else(missing_field("payload"))? {
+ ipfs::Ipld::Link(cid) => cid.clone(),
+ _ => return field_wrong_type("payload", "Link")
+ };
+
+ Ok(Node {
+ version,
+ parents,
+ payload
+ })
+ }
+
+ _ => anyhow::bail!("Unexpected type, expected map")
+ }
+ }
+}
+
+impl Node {
+ pub fn new(version: String, parents: Vec<ipfs::Cid>, payload: ipfs::Cid) -> Self {
+ Self {
+ version,
+ parents,
+ payload,
+ }
+ }
+
+ pub fn parents(&self) -> Vec<ipfs::Cid> {
+ self.parents.clone()
+ }
+
+ pub fn payload(&self) -> ipfs::Cid {
+ self.payload.clone()
+ }
+}
diff --git a/lib/src/types/payload.rs b/lib/src/types/payload.rs
new file mode 100644
index 0000000..a11b215
--- /dev/null
+++ b/lib/src/types/payload.rs
@@ -0,0 +1,72 @@
+use std::convert::TryFrom;
+
+use anyhow::Result;
+
+use crate::types::DateTime;
+
+#[derive(Debug, Eq, PartialEq, getset::Getters)]
+pub struct Payload {
+ // TODO: Make this a mime::Mime, but as this type does not impl Serialize/Deserialize, we
+ // cannot do this trivially yet
+ #[getset(get = "pub")]
+ mime: String,
+
+ #[getset(get = "pub")]
+ timestamp: DateTime,
+
+ content: ipfs::Cid,
+}
+
+impl Into<ipfs::Ipld> for Payload {
+ fn into(self) -> ipfs::Ipld {
+ let mut map = std::collections::BTreeMap::new();
+ map.insert(String::from("mime"), ipfs::Ipld::String(self.mime));
+ map.insert(String::from("timestamp"), self.timestamp.into());
+ map.insert(String::from("content"), ipfs::Ipld::Link(self.content));
+ ipfs::Ipld::Map(map)
+ }
+}
+
+impl TryFrom<ipfs::Ipld> for Payload {
+ type Error = anyhow::Error;
+
+ fn try_from(ipld: ipfs::Ipld) -> Result<Self> {
+ let missing_field = |name: &'static str| move || anyhow::anyhow!("Missing field {}", name);
+ let field_wrong_type = |name: &str, expty: &str| anyhow::bail!("Field {} has wrong type, expected {}", name, expty);
+ match ipld {
+ ipfs::Ipld::Map(map) => {
+ let mime = match map.get("mime").ok_or_else(missing_field("mime"))? {
+ ipfs::Ipld::String(s) => s.to_owned(),
+ _ => return field_wrong_type("mime", "String")
+ };
+
+ let timestamp = map.get("timestamp")
+ .ok_or_else(missing_field("timestamp"))?;
+ let timestamp = DateTime::try_from(timestamp.clone())?; // TODO dont clone
+
+ let content = match map.get("content").ok_or_else(missing_field("content"))? {
+ ipfs::Ipld::Link(cid) => cid.clone(),
+ _ => return field_wrong_type("content", "Link")
+ };
+
+ Ok(Payload {
+ mime,
+ timestamp,
+ content
+ })
+ },
+
+ _ => anyhow::bail!("Unexpected type, expected map"),
+ }
+ }
+}
+
+impl Payload {
+ pub fn new(mime: String, timestamp: DateTime, content: ipfs::Cid) -> Self {
+ Self { mime, timestamp, content: content.into() }
+ }
+
+ pub fn content(&self) -> ipfs::Cid {
+ self.content.clone()
+ }
+}