Skip to main content

pubky_homeserver/data_directory/
persistent_data_dir.rs

1use super::{data_dir::DataDir, ConfigToml};
2
3use std::{
4    io::Write,
5    path::{Path, PathBuf},
6};
7
8/// The data directory for the homeserver.
9///
10/// This is the directory that will store the homeservers data.
11///
12#[derive(Debug, Clone)]
13pub struct PersistentDataDir {
14    expanded_path: PathBuf,
15}
16
17impl PersistentDataDir {
18    /// Creates a new data directory.
19    /// `path` will be expanded to the home directory if it starts with "~".
20    pub fn new(path: PathBuf) -> Self {
21        Self {
22            expanded_path: Self::expand_home_dir(path),
23        }
24    }
25
26    /// Expands the data directory to the home directory if it starts with "~".
27    /// Return the full path to the data directory.
28    fn expand_home_dir(path: PathBuf) -> PathBuf {
29        let path = match path.to_str() {
30            Some(path) => path,
31            None => {
32                // Path not valid utf-8 so we can't expand it.
33                return path;
34            }
35        };
36
37        if path.starts_with("~/") {
38            if let Some(home) = dirs::home_dir() {
39                let without_home = path.strip_prefix("~/").expect("Invalid ~ prefix");
40                let joined = home.join(without_home);
41                return joined;
42            }
43        }
44        PathBuf::from(path)
45    }
46
47    /// Returns the config file path in this directory.
48    pub fn get_config_file_path(&self) -> PathBuf {
49        self.expanded_path.join("config.toml")
50    }
51
52    fn write_sample_config_file(&self) -> anyhow::Result<()> {
53        let config_string = ConfigToml::sample_string();
54        let config_file_path = self.get_config_file_path();
55        let mut config_file = std::fs::File::create(config_file_path)?;
56        config_file.write_all(config_string.as_bytes())?;
57        Ok(())
58    }
59
60    /// Returns the path to the secret file.
61    pub fn get_secret_file_path(&self) -> PathBuf {
62        self.expanded_path.join("secret")
63    }
64}
65
66impl Default for PersistentDataDir {
67    fn default() -> Self {
68        Self::new(PathBuf::from("~/.pubky"))
69    }
70}
71
72impl DataDir for PersistentDataDir {
73    /// Returns the full path to the data directory.
74    fn path(&self) -> &Path {
75        &self.expanded_path
76    }
77
78    /// Makes sure the data directory exists.
79    /// Create the directory if it doesn't exist.
80    fn ensure_data_dir_exists_and_is_writable(&self) -> anyhow::Result<()> {
81        std::fs::create_dir_all(&self.expanded_path)?;
82
83        // Check if we can write to the data directory
84        let test_file_path = self
85            .expanded_path
86            .join("test_write_f2d560932f9b437fa9ef430ba436d611"); // random file name to not conflict with anything
87        std::fs::write(test_file_path.clone(), b"test")
88            .map_err(|err| anyhow::anyhow!("Failed to write to data directory: {}", err))?;
89        std::fs::remove_file(test_file_path)
90            .map_err(|err| anyhow::anyhow!("Failed to write to data directory: {}", err))?;
91        Ok(())
92    }
93
94    /// Reads the config file from the data directory.
95    /// Creates a default config file if it doesn't exist.
96    fn read_or_create_config_file(&self) -> anyhow::Result<ConfigToml> {
97        let config_file_path = self.get_config_file_path();
98        if !config_file_path.exists() {
99            self.write_sample_config_file()?;
100        }
101        let config = ConfigToml::from_file(config_file_path)?;
102        Ok(config)
103    }
104
105    /// Reads the secret file. Creates a new secret file if it doesn't exist.
106    fn read_or_create_keypair(&self) -> anyhow::Result<pubky_common::crypto::Keypair> {
107        let secret_file_path = self.get_secret_file_path();
108        if !secret_file_path.exists() {
109            // Create a new secret file
110            pubky_common::crypto::Keypair::random().write_secret_key_file(&secret_file_path)?;
111            tracing::info!("Secret file created at {}", secret_file_path.display());
112        }
113        // Read the secret file
114        let keypair = pubky_common::crypto::Keypair::from_secret_key_file(&secret_file_path)?;
115        Ok(keypair)
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::io::Write;
122
123    use super::*;
124    use tempfile::TempDir;
125
126    /// Test that the home directory is expanded correctly.
127    #[test]
128    pub fn test_expand_home_dir() {
129        let data_dir = PersistentDataDir::new(PathBuf::from("~/.pubky"));
130        let homedir = dirs::home_dir().unwrap();
131        let expanded_path = homedir.join(".pubky");
132        assert_eq!(data_dir.expanded_path, expanded_path);
133    }
134
135    /// Test that the data directory is created if it doesn't exist.
136    #[test]
137    pub fn test_ensure_data_dir_exists_and_is_accessible() {
138        let temp_dir = TempDir::new().unwrap();
139        let test_path = temp_dir.path().join(".pubky");
140        let data_dir = PersistentDataDir::new(test_path.clone());
141
142        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
143        assert!(test_path.exists());
144        // temp_dir will be automatically cleaned up when it goes out of scope
145    }
146
147    #[test]
148    pub fn test_get_default_config_file_path_exists() {
149        let temp_dir = TempDir::new().unwrap();
150        let test_path = temp_dir.path().join(".pubky");
151        let data_dir = PersistentDataDir::new(test_path.clone());
152        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
153        let config_file_path = data_dir.get_config_file_path();
154        assert!(!config_file_path.exists()); // Should not exist yet
155
156        let mut config_file = std::fs::File::create(config_file_path.clone()).unwrap();
157        config_file.write_all(b"test").unwrap();
158        assert!(config_file_path.exists()); // Should exist now
159                                            // temp_dir will be automatically cleaned up when it goes out of scope
160    }
161
162    #[test]
163    pub fn test_read_or_create_config_file() {
164        let temp_dir = TempDir::new().unwrap();
165        let test_path = temp_dir.path().join(".pubky");
166        let data_dir = PersistentDataDir::new(test_path.clone());
167        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
168        let _ = data_dir.read_or_create_config_file().unwrap(); // Should create a default config file
169        assert!(data_dir.get_config_file_path().exists());
170
171        let _ = data_dir.read_or_create_config_file().unwrap(); // Should read the existing file
172        assert!(data_dir.get_config_file_path().exists());
173    }
174
175    #[test]
176    pub fn test_read_or_create_config_file_dont_override_existing_file() {
177        let temp_dir = TempDir::new().unwrap();
178        let test_path = temp_dir.path().join(".pubky");
179        let data_dir = PersistentDataDir::new(test_path.clone());
180        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
181
182        // Write a broken config file
183        let config_file_path = data_dir.get_config_file_path();
184        std::fs::write(config_file_path.clone(), b"test").unwrap();
185        assert!(config_file_path.exists()); // Should exist now
186
187        // Try to read the config file and fail because config is broken
188        let read_result = data_dir.read_or_create_config_file();
189        assert!(read_result.is_err());
190
191        // Make sure the broken config file is still there
192        let content = std::fs::read_to_string(config_file_path).unwrap();
193        assert_eq!(content, "test");
194    }
195
196    #[test]
197    pub fn test_create_secret_file() {
198        let temp_dir = TempDir::new().unwrap();
199        let test_path = temp_dir.path().join(".pubky");
200        let data_dir = PersistentDataDir::new(test_path.clone());
201        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
202
203        let _ = data_dir.read_or_create_keypair().unwrap();
204        assert!(data_dir.get_secret_file_path().exists());
205    }
206
207    #[test]
208    pub fn test_dont_override_existing_secret_file() {
209        let temp_dir = TempDir::new().unwrap();
210        let test_path = temp_dir.path().join(".pubky");
211        let data_dir = PersistentDataDir::new(test_path.clone());
212        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
213
214        // Create a secret file
215        let secret_file_path = data_dir.get_secret_file_path();
216        std::fs::write(secret_file_path.clone(), b"test").unwrap();
217
218        let result = data_dir.read_or_create_keypair();
219        assert!(result.is_err());
220        assert!(data_dir.get_secret_file_path().exists());
221        let content = std::fs::read_to_string(secret_file_path).unwrap();
222        assert_eq!(content, "test");
223    }
224
225    #[test]
226    pub fn test_trim_secret_file_content() {
227        let temp_dir = TempDir::new().unwrap();
228        let test_path = temp_dir.path().join(".pubky");
229        let data_dir = PersistentDataDir::new(test_path.clone());
230        data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
231
232        // Create a secret file
233        let keypair = pubky_common::crypto::Keypair::random();
234        let secret_file_path = data_dir.get_secret_file_path();
235        let file_content = format!("\n {}\n \n", hex::encode(keypair.secret_key()));
236        std::fs::write(secret_file_path.clone(), file_content).unwrap();
237
238        let result = data_dir.read_or_create_keypair();
239        assert!(result.is_ok());
240        let read_keypair = result.unwrap();
241        assert_eq!(read_keypair.secret_key(), keypair.secret_key());
242    }
243}