use super::{data_dir::DataDir, ConfigToml};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
io::Write,
path::{Path, PathBuf},
};
#[derive(Debug, Clone)]
pub struct PersistentDataDir {
expanded_path: PathBuf,
}
impl PersistentDataDir {
pub fn new(path: PathBuf) -> Self {
Self {
expanded_path: Self::expand_home_dir(path),
}
}
fn expand_home_dir(path: PathBuf) -> PathBuf {
let path = match path.to_str() {
Some(path) => path,
None => {
return path;
}
};
if path.starts_with("~/") {
if let Some(home) = dirs::home_dir() {
let without_home = path.strip_prefix("~/").expect("Invalid ~ prefix");
let joined = home.join(without_home);
return joined;
}
}
PathBuf::from(path)
}
pub fn get_config_file_path(&self) -> PathBuf {
self.expanded_path.join("config.toml")
}
fn write_sample_config_file(&self) -> anyhow::Result<()> {
let config_string = ConfigToml::sample_string();
let config_file_path = self.get_config_file_path();
let mut config_file = std::fs::File::create(config_file_path)?;
config_file.write_all(config_string.as_bytes())?;
Ok(())
}
pub fn get_secret_file_path(&self) -> PathBuf {
self.expanded_path.join("secret")
}
pub fn write_keypair(&self, keypair: &pkarr::Keypair) -> anyhow::Result<()> {
let secret_file_path = self.get_secret_file_path();
let secret = keypair.secret_key();
let hex_string = hex::encode(secret);
std::fs::write(secret_file_path.clone(), hex_string)?;
#[cfg(unix)]
{
std::fs::set_permissions(&secret_file_path, std::fs::Permissions::from_mode(0o600))?;
}
tracing::info!("Secret file created at {}", secret_file_path.display());
Ok(())
}
}
impl Default for PersistentDataDir {
fn default() -> Self {
Self::new(PathBuf::from("~/.pubky"))
}
}
impl DataDir for PersistentDataDir {
fn path(&self) -> &Path {
&self.expanded_path
}
fn ensure_data_dir_exists_and_is_writable(&self) -> anyhow::Result<()> {
std::fs::create_dir_all(&self.expanded_path)?;
let test_file_path = self
.expanded_path
.join("test_write_f2d560932f9b437fa9ef430ba436d611"); std::fs::write(test_file_path.clone(), b"test")
.map_err(|err| anyhow::anyhow!("Failed to write to data directory: {}", err))?;
std::fs::remove_file(test_file_path)
.map_err(|err| anyhow::anyhow!("Failed to write to data directory: {}", err))?;
Ok(())
}
fn read_or_create_config_file(&self) -> anyhow::Result<ConfigToml> {
let config_file_path = self.get_config_file_path();
if !config_file_path.exists() {
self.write_sample_config_file()?;
}
let config = ConfigToml::from_file(config_file_path)?;
Ok(config)
}
fn read_or_create_keypair(&self) -> anyhow::Result<pkarr::Keypair> {
let secret_file_path = self.get_secret_file_path();
if !secret_file_path.exists() {
self.write_keypair(&pkarr::Keypair::random())?;
}
let secret = std::fs::read(secret_file_path)?;
let secret_string = String::from_utf8_lossy(&secret).trim().to_string();
let secret_bytes = hex::decode(secret_string)?;
let secret_bytes: [u8; 32] = secret_bytes.try_into().map_err(|_| {
anyhow::anyhow!("Failed to convert secret bytes into array of length 32")
})?;
let keypair = pkarr::Keypair::from_secret_key(&secret_bytes);
Ok(keypair)
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
use tempfile::TempDir;
#[test]
pub fn test_expand_home_dir() {
let data_dir = PersistentDataDir::new(PathBuf::from("~/.pubky"));
let homedir = dirs::home_dir().unwrap();
let expanded_path = homedir.join(".pubky");
assert_eq!(data_dir.expanded_path, expanded_path);
}
#[test]
pub fn test_ensure_data_dir_exists_and_is_accessible() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = PersistentDataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
assert!(test_path.exists());
}
#[test]
pub fn test_get_default_config_file_path_exists() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = PersistentDataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let config_file_path = data_dir.get_config_file_path();
assert!(!config_file_path.exists());
let mut config_file = std::fs::File::create(config_file_path.clone()).unwrap();
config_file.write_all(b"test").unwrap();
assert!(config_file_path.exists()); }
#[test]
pub fn test_read_or_create_config_file() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = PersistentDataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let _ = data_dir.read_or_create_config_file().unwrap(); assert!(data_dir.get_config_file_path().exists());
let _ = data_dir.read_or_create_config_file().unwrap(); assert!(data_dir.get_config_file_path().exists());
}
#[test]
pub fn test_read_or_create_config_file_dont_override_existing_file() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = PersistentDataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let config_file_path = data_dir.get_config_file_path();
std::fs::write(config_file_path.clone(), b"test").unwrap();
assert!(config_file_path.exists());
let read_result = data_dir.read_or_create_config_file();
assert!(read_result.is_err());
let content = std::fs::read_to_string(config_file_path).unwrap();
assert_eq!(content, "test");
}
#[test]
pub fn test_create_secret_file() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = PersistentDataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let _ = data_dir.read_or_create_keypair().unwrap();
assert!(data_dir.get_secret_file_path().exists());
}
#[test]
pub fn test_dont_override_existing_secret_file() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = PersistentDataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let secret_file_path = data_dir.get_secret_file_path();
std::fs::write(secret_file_path.clone(), b"test").unwrap();
let result = data_dir.read_or_create_keypair();
assert!(result.is_err());
assert!(data_dir.get_secret_file_path().exists());
let content = std::fs::read_to_string(secret_file_path).unwrap();
assert_eq!(content, "test");
}
#[test]
pub fn test_trim_secret_file_content() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = PersistentDataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let keypair = pkarr::Keypair::random();
let secret_file_path = data_dir.get_secret_file_path();
let file_content = format!("\n {}\n \n", hex::encode(keypair.secret_key()));
std::fs::write(secret_file_path.clone(), file_content).unwrap();
let result = data_dir.read_or_create_keypair();
assert!(result.is_ok());
let read_keypair = result.unwrap();
assert_eq!(read_keypair.secret_key(), keypair.secret_key());
}
}