diff --git a/src/storage.rs b/src/storage.rs index 08c051a..525ce77 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -2,14 +2,11 @@ use std::{ collections::BTreeSet, - env, fs, - fs::OpenOptions, - io::Write, + env, path::{Path, PathBuf}, }; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; -use directories::ProjectDirs; use iroh::{EndpointAddr, SecretKey}; use rusqlite::{Connection, OptionalExtension, params}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; @@ -20,87 +17,11 @@ use crate::{ protocol::Invite, }; -pub struct AppPaths { - pub data_directory: PathBuf, - database_path: PathBuf, - identity_path: PathBuf, -} - -const IDENTITY_BACKUP_PREFIX: &str = "appa-identity-v1:"; -const DEFAULT_HISTORY_REVISIONS: usize = 100; - -impl AppPaths { - pub fn discover() -> anyhow::Result { - let data_directory = match env::var_os("APPA_HOME") { - Some(path) => PathBuf::from(path), - None => ProjectDirs::from("dev", "Appa", "Appa") - .ok_or_else(|| anyhow::anyhow!("could not determine Appa data directory"))? - .data_local_dir() - .to_owned(), - }; - Self::from_data_directory(data_directory) - } - - pub fn from_data_directory(data_directory: PathBuf) -> anyhow::Result { - fs::create_dir_all(&data_directory)?; - Ok(Self { - database_path: data_directory.join("state.sqlite3"), - identity_path: data_directory.join("identity.key"), - data_directory, - }) - } - - pub fn load_identity(&self) -> anyhow::Result { - if self.identity_path.exists() { - return read_identity(&self.identity_path); - } - let identity = SecretKey::generate(); - write_identity(&self.identity_path, &identity)?; - Ok(identity) - } - - pub fn export_identity(&self, destination: &Path) -> anyhow::Result<()> { - let identity = self.load_identity()?; - let mut backup = OpenOptions::new() - .write(true) - .create_new(true) - .open(destination)?; - backup.write_all(IDENTITY_BACKUP_PREFIX.as_bytes())?; - backup.write_all(URL_SAFE_NO_PAD.encode(identity.to_bytes()).as_bytes())?; - backup.write_all(b"\n")?; - Ok(()) - } - - pub fn import_identity(&self, source: &Path, replace_existing: bool) -> anyhow::Result<()> { - if self.identity_path.exists() && !replace_existing { - anyhow::bail!("an Appa identity already exists; pass --yes to replace it"); - } - let encoded_identity = fs::read_to_string(source)?; - let encoded_identity = encoded_identity - .trim() - .strip_prefix(IDENTITY_BACKUP_PREFIX) - .ok_or_else(|| anyhow::anyhow!("identity backup has an unsupported format"))?; - let identity = identity_from_encoded(encoded_identity)?; - write_identity(&self.identity_path, &identity) - } -} +mod paths; -fn read_identity(identity_path: &Path) -> anyhow::Result { - identity_from_encoded(fs::read_to_string(identity_path)?.trim()) -} +pub use paths::AppPaths; -fn identity_from_encoded(encoded_identity: &str) -> anyhow::Result { - let key_bytes = URL_SAFE_NO_PAD.decode(encoded_identity)?; - let key_bytes: [u8; 32] = key_bytes - .try_into() - .map_err(|_| anyhow::anyhow!("Appa identity has an invalid length"))?; - Ok(SecretKey::from_bytes(&key_bytes)) -} - -fn write_identity(identity_path: &Path, identity: &SecretKey) -> anyhow::Result<()> { - fs::write(identity_path, URL_SAFE_NO_PAD.encode(identity.to_bytes()))?; - Ok(()) -} +const DEFAULT_HISTORY_REVISIONS: usize = 100; pub struct StateStore { connection: Connection, diff --git a/src/storage/paths.rs b/src/storage/paths.rs new file mode 100644 index 0000000..44ff348 --- /dev/null +++ b/src/storage/paths.rs @@ -0,0 +1,93 @@ +use std::{ + env, fs, + fs::OpenOptions, + io::Write, + path::{Path, PathBuf}, +}; + +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use directories::ProjectDirs; +use iroh::SecretKey; + +const IDENTITY_BACKUP_PREFIX: &str = "appa-identity-v1:"; + +pub struct AppPaths { + pub data_directory: PathBuf, + pub(super) database_path: PathBuf, + identity_path: PathBuf, +} + +impl AppPaths { + pub fn discover() -> anyhow::Result { + let data_directory = match env::var_os("APPA_HOME") { + Some(path) => PathBuf::from(path), + None => ProjectDirs::from("dev", "Appa", "Appa") + .ok_or_else(|| anyhow::anyhow!("could not determine Appa data directory"))? + .data_local_dir() + .to_owned(), + }; + Self::from_data_directory(data_directory) + } + + pub fn from_data_directory(data_directory: PathBuf) -> anyhow::Result { + fs::create_dir_all(&data_directory)?; + Ok(Self { + database_path: data_directory.join("state.sqlite3"), + identity_path: data_directory.join("identity.key"), + data_directory, + }) + } + + pub fn load_identity(&self) -> anyhow::Result { + if self.identity_path.exists() { + return read_identity(&self.identity_path); + } + let identity = SecretKey::generate(); + write_identity(&self.identity_path, &identity)?; + Ok(identity) + } + + pub fn export_identity(&self, destination: &Path) -> anyhow::Result<()> { + let identity = self.load_identity()?; + let mut backup = OpenOptions::new() + .write(true) + .create_new(true) + .open(destination)?; + backup.write_all(IDENTITY_BACKUP_PREFIX.as_bytes())?; + backup.write_all(URL_SAFE_NO_PAD.encode(identity.to_bytes()).as_bytes())?; + backup.write_all(b"\n")?; + Ok(()) + } + + pub fn import_identity(&self, source: &Path, replace_existing: bool) -> anyhow::Result<()> { + if self.identity_path.exists() && !replace_existing { + anyhow::bail!("an Appa identity already exists; pass --yes to replace it"); + } + let encoded_identity = fs::read_to_string(source)?; + let encoded_identity = encoded_identity + .trim() + .strip_prefix(IDENTITY_BACKUP_PREFIX) + .ok_or_else(|| anyhow::anyhow!("identity backup has an unsupported format"))?; + write_identity( + &self.identity_path, + &identity_from_encoded(encoded_identity)?, + ) + } +} + +fn read_identity(identity_path: &Path) -> anyhow::Result { + identity_from_encoded(fs::read_to_string(identity_path)?.trim()) +} + +fn identity_from_encoded(encoded_identity: &str) -> anyhow::Result { + let key_bytes = URL_SAFE_NO_PAD.decode(encoded_identity)?; + let key_bytes: [u8; 32] = key_bytes + .try_into() + .map_err(|_| anyhow::anyhow!("Appa identity has an invalid length"))?; + Ok(SecretKey::from_bytes(&key_bytes)) +} + +fn write_identity(identity_path: &Path, identity: &SecretKey) -> anyhow::Result<()> { + fs::write(identity_path, URL_SAFE_NO_PAD.encode(identity.to_bytes()))?; + Ok(()) +}