summaryrefslogtreecommitdiffstats
path: root/lib/src/profile
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2021-12-08 18:55:17 +0100
committerMatthias Beyer <mail@beyermatthias.de>2021-12-08 18:55:17 +0100
commitda8e93414399157e1dd66f527c7466a1e7218807 (patch)
tree61fe96501952d9965bb077d57a5257b519ffc7e8 /lib/src/profile
parentcd93da65aa447d3bf4ec53fbad040e77305b38c3 (diff)
parent7863d21153afd293226c9889610379ab94b0b6f6 (diff)
Merge branch 'iced-gui'
Diffstat (limited to 'lib/src/profile')
-rw-r--r--lib/src/profile/mod.rs181
-rw-r--r--lib/src/profile/state.rs129
2 files changed, 310 insertions, 0 deletions
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)
+ },
+ })
+ }
+}
+
+