config.rs
1 //! Logic to interact with configuration file. 2 use anyhow::anyhow; 3 use serde::{Deserialize, Serialize}; 4 use std::{collections::HashMap, fs::{self, File}, io::{self, BufWriter}, path::{Path, PathBuf}, str::FromStr}; 5 use tempfile::tempdir; 6 use walkdir::WalkDir; 7 use zip::{write::{ExtendedFileOptions, FileOptions}, CompressionMethod, ZipArchive, ZipWriter}; 8 /// Type alias for the contents of a path. 9 type Contents = HashMap<String, Node>; 10 /// Contains the possible results of a node in the configuration. 11 #[derive(Serialize, Deserialize)] 12 enum Node { 13 /// File definition. 14 File {}, 15 /// Directory definition. 16 Folder(Contents), 17 } 18 /// Contains all configuration information for the program. 19 /// 20 /// # Fields 21 /// - `path`: The path of the configuration file. 22 /// - `username`: The username to use in place of $USER. 23 /// - `root`: The file hierarchy stored in the configuration file. 24 #[derive(Serialize, Deserialize)] 25 pub struct Config { 26 path: PathBuf, 27 username: String, 28 root: Contents, 29 } 30 impl Config { 31 /// Reads the configuration file from the path. 32 /// If the file doesn't exist, an empty configuration is made. 33 /// 34 /// # Fields 35 /// - `config_path`: 36 /// - `username`: 37 /// 38 /// # Examples 39 /// ``` 40 /// // TODO: Config::new() example 41 /// ``` 42 pub fn new(config_path: PathBuf, username: Option<String>) -> anyhow::Result<Self> { 43 let path = config_path.clone(); 44 let username = username.unwrap_or(whoami::username()); 45 let mut config = match config_path.exists() { 46 true => Self { 47 path, 48 username, 49 root: serde_json::from_str(&fs::read_to_string(&config_path)?).unwrap_or_default(), 50 }, 51 false => { 52 File::create(&config_path)?; 53 Self { 54 path, 55 username, 56 root: HashMap::new(), 57 } 58 }, 59 }; 60 config.add_path(&config_path)?; 61 Ok(config) 62 } 63 /// Reads the configuration file from the default location ({os_config_dir}/dotfiles.conf). 64 /// 65 /// # Fields 66 /// - `username`: 67 /// 68 /// # Examples 69 /// ``` 70 /// // TODO: Config::default_config() example 71 /// ``` 72 pub fn default_config(username: Option<String>) -> anyhow::Result<Self> { 73 match dirs::config_dir() { 74 Some(path) => Self::new(path.join("dotfiles.conf"), username) , 75 None => Err(anyhow!("Current user does not have a home directory")), 76 } 77 } 78 /// Adds a path to the configuration. 79 /// 80 /// # Fields 81 /// - `path`: The path to be added. 82 /// 83 /// # Examples 84 /// ``` 85 /// // TODO: add_path() example 86 /// ``` 87 pub fn add_path(&mut self, path: &PathBuf) -> anyhow::Result<()> { 88 let path = fs::canonicalize(path)?; 89 match path { 90 path if path.is_file() => self.add_file(&path), 91 path if path.is_dir() => self.add_dir(&path), 92 _ => Err(anyhow!("Attempted to read config from {}, but path does not exist", path.to_string_lossy())), 93 }?; 94 self.save() 95 } 96 /// Removes a path from the configuration. 97 /// 98 /// # Fields 99 /// - `path`: The path to be removed. 100 /// 101 /// # Examples 102 /// ``` 103 /// // TODO: remove_path() example 104 /// ``` 105 pub fn remove_path(&mut self, path: &PathBuf) -> anyhow::Result<()> { 106 let path = fs::canonicalize(path)?; 107 let components = self.username_to_placeholder(&path); 108 Self::remove_recursive(&mut self.root, &components); 109 self.save() 110 } 111 /// Exports the configuration to a zip archive. 112 /// 113 /// # Fields 114 /// - `path`: The path for the zip archive to be made at. 115 /// - `overwrite`: Overwrite existing files. 116 /// 117 /// # Examples 118 /// ``` 119 /// // TODO: export() example 120 /// ``` 121 pub fn export(&self, path: &Option<PathBuf>, overwrite: bool) -> anyhow::Result<()> { 122 let path = get_path(path)?; 123 match (path.exists(), overwrite) { 124 (true, false) => Err(anyhow!("The requested export path already exists. If this was intentional rerun with the overwrite flag")), 125 _ => self.make_archive(&path), 126 } 127 } 128 /// Installs a configuration from a zip archive. 129 /// # Fields 130 /// - `source_path`: The path of the zip archive to be installed. 131 /// - `destination_path`: The base path for the extraction. 132 /// 133 /// # Examples 134 /// ``` 135 /// //TODO: extract_from_zip() example 136 /// ``` 137 pub fn extract_from_zip(&self, source_path: &Path, destination_path: &Path) -> anyhow::Result<()> { 138 let mut archive = ZipArchive::new(File::open(source_path)?)?; 139 for i in 0..archive.len() { 140 let mut file = archive.by_index(i)?; 141 if file.name().eq("README.md") || file.name().eq("LICENSE") { continue; } 142 let path = self.placeholder_to_username(&Path::new(destination_path).join(file.name())); 143 match file.is_dir() { 144 true => fs::create_dir_all(&path)?, 145 false => { 146 if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } 147 io::copy(&mut file, &mut File::create(&path)?)?; 148 } 149 } 150 } 151 Ok(()) 152 } 153 /// Converts username in path to placeholder string "$USER". 154 /// 155 /// # Fields 156 /// - `path`: The path to be updated. 157 /// 158 /// # Examples 159 /// ``` 160 /// // TODO: username_to_placeholder() example 161 /// ``` 162 fn username_to_placeholder(&self, path: &PathBuf) -> Vec<String> { 163 path.iter().map(|part| { 164 let part = part.to_string_lossy().to_string(); 165 match part.eq(&self.username) { 166 true => "$USER".to_string(), 167 false => part, 168 } 169 }).collect() 170 } 171 /// Converts placeholder string "$USER" in path to username. 172 /// 173 /// # Fields 174 /// - `path`: The path to be updated. 175 /// 176 /// # Examples 177 /// ``` 178 /// // TODO: placeholder_to_username() example 179 /// ``` 180 fn placeholder_to_username(&self, path: &PathBuf) -> PathBuf { 181 path.iter().map(|part| { 182 let part = part.to_string_lossy().to_string(); 183 match part.eq("$USER") { 184 true => self.username.to_string(), 185 false => part, 186 } 187 }).collect() 188 } 189 /// Add a file to the configuration. 190 /// 191 /// # Fields 192 /// - `file_path`: The path of the file to be added. 193 /// 194 /// # Examples 195 /// ``` 196 /// // TODO: add_file() example 197 /// ``` 198 fn add_file(&mut self, file_path: &PathBuf) -> anyhow::Result<()> { 199 let components = self.username_to_placeholder(file_path); 200 let mut current = &mut self.root; 201 for (i, part) in components.iter().enumerate() { 202 let is_last = i == file_path.iter().count() - 1; 203 if !current.contains_key(part) { 204 match is_last { 205 true => current.insert(part.clone(), Node::File {}), 206 false => current.insert(part.clone(), Node::Folder(HashMap::new())) 207 }; 208 } 209 match current.get_mut(part) { 210 Some(Node::Folder(map)) => { current = map; }, 211 Some(Node::File {}) if is_last => return Ok(()), 212 Some(Node::File {}) => return Err(anyhow!("{} is file, but a directory was expected at this path", part)), 213 None => unreachable!(), 214 } 215 } 216 Ok(()) 217 } 218 /// Add a directory to the configuration. 219 /// 220 /// # Fields 221 /// - `dir_path`: The path of the directory to be added. 222 /// 223 /// # Examples 224 /// ``` 225 /// // TODO: add_dir() example 226 /// ``` 227 fn add_dir(&mut self, dir_path: &PathBuf) -> anyhow::Result<()> { 228 for entry in fs::read_dir(dir_path)? { 229 self.add_path(&entry?.path())?; 230 } 231 Ok(()) 232 } 233 /// Remove a Node from the configuration recursively. 234 /// 235 /// # Fields 236 /// - `map`: The configuration map to edit. 237 /// - `components`: The nodes to be removed. 238 /// 239 /// # Examples 240 /// ``` 241 /// // TODO: remove_recursive() example 242 /// ``` 243 fn remove_recursive(map: &mut Contents, components: &Vec<String>) -> bool { 244 if components.is_empty() { return false; } 245 let key = &components[0]; 246 247 if components.len() == 1 { map.remove(key).is_some() } 248 else if let Some(Node::Folder(child_map)) = map.get_mut(key) { 249 let removed = Self::remove_recursive(child_map, &components[1..].to_vec()); 250 if child_map.is_empty() { map.remove(key); } 251 removed 252 } 253 else { false } 254 } 255 /// Save the configuration to it's file path. 256 /// 257 /// # Examples 258 /// ``` 259 /// // TODO: save() example 260 /// ``` 261 fn save(&self) -> anyhow::Result<()> { 262 let file = File::create(&self.path)?; 263 serde_json::to_writer_pretty(BufWriter::new(file), &self.root)?; 264 Ok(()) 265 } 266 /// Produce a zip archive that contains the referenced files in the configuration. 267 /// 268 /// # Fields 269 /// - `archive_path`: The path of the new archive. 270 /// 271 /// # Examples 272 /// ``` 273 /// // TODO: make_archive() example 274 /// ``` 275 fn make_archive(&self, archive_path: &PathBuf) -> anyhow::Result<()> { 276 let tmp_dir = tempdir()?; 277 self.copy_files_recursive(&Path::new("/"), tmp_dir.path(), &self.root)?; 278 compress_to_zip(&tmp_dir.path(), &archive_path)?; 279 tmp_dir.close()?; 280 Ok(()) 281 } 282 /// Recursively copy files from a path to a new location. 283 /// This function only copies files that are also stored in the configuration file. 284 /// 285 /// # Fields 286 /// - `source_path`: The path to copy files from. 287 /// - `destination_path`: The base path to copy the files to. 288 /// - `node`: The node in the configuration that matches the source_path. 289 /// 290 /// # Examples 291 /// ``` 292 /// //TODO: copy_files_recursive() example 293 /// ``` 294 fn copy_files_recursive(&self, source_path: &Path, destination_path: &Path, node: &Contents) -> anyhow::Result<()> { 295 for (name, subnode) in node { 296 let name = match name { 297 name if name.eq("/") => "", 298 _ => name, 299 }; 300 let new_src = self.placeholder_to_username(&source_path.join(name)); 301 let new_dst = destination_path.join(&name); 302 match subnode { 303 Node::File {} => { fs::copy(new_src, new_dst)?; }, 304 Node::Folder(child_map) => { 305 if !name.eq("/") { fs::create_dir_all(&new_dst)?; } 306 self.copy_files_recursive(&new_src, &new_dst, child_map)?; 307 } 308 } 309 } 310 Ok(()) 311 } 312 } 313 /// Reads the path. 314 /// If none, returns the default (./dotfiles.zip). 315 /// Otherwise, returns the path. 316 /// 317 /// # Fields: 318 /// - `path`: The path to be read. 319 /// 320 /// # Examples 321 /// ``` 322 /// // TODO: get_path() example 323 /// ``` 324 fn get_path(path: &Option<PathBuf>) -> anyhow::Result<PathBuf> { 325 let mut path = match path { 326 Some(path) => path.clone(), 327 None => PathBuf::from_str("./dotfiles.zip")?, 328 }; 329 match path.extension() { 330 Some(_ext) => (), 331 None => { path.set_extension("zip"); }, 332 }; 333 Ok(path) 334 } 335 /// Compresses a path to a zip archive at the destination path. 336 /// 337 /// # Fields 338 /// - `source_path`: The path to be compressed. 339 /// - `destination_path`: The path the archive should be made at. 340 /// 341 /// # Examples 342 /// ``` 343 /// //TODO: compress_to_zip() example 344 /// ``` 345 fn compress_to_zip(source_path: &Path, destination_path: &Path) -> anyhow::Result<()> { 346 let mut zip_writer = ZipWriter::new(File::create(destination_path)?); 347 let options: FileOptions<'_, ExtendedFileOptions> = FileOptions::default() 348 .compression_method(CompressionMethod::Deflated) 349 .unix_permissions(0o755); 350 for entry in WalkDir::new(source_path) { 351 let entry = entry?; 352 let path = entry.path(); 353 let rel_path = path.strip_prefix(source_path)?.to_string_lossy(); 354 355 if path.is_file() { 356 zip_writer.start_file(rel_path, options.clone())?; 357 let mut f = File::open(path)?; 358 io::copy(&mut f, &mut zip_writer)?; 359 } else if path.is_dir() { 360 zip_writer.add_directory(rel_path, options.clone())?; 361 } 362 } 363 zip_writer.finish()?; 364 Ok(()) 365 }