add.rs
1 use std::{ 2 fs::{self, create_dir_all, symlink_metadata}, 3 io::{ErrorKind, Write as _, stdin, stdout}, 4 os::unix::fs::symlink, 5 path::Path, 6 process::exit, 7 }; 8 9 use anyhow::{Context as _, Result, ensure}; 10 11 use crate::util::{config_path, paths_equal, rerun_with_root, system_path}; 12 13 /// Symlink a the given path to its location in the actual system 14 pub fn add(path: &Path, force: bool, copy: bool) -> Result<()> { 15 if copy { 16 return add_copy(path, force); 17 } 18 19 let config_path = config_path(path)?; 20 let system_path = system_path(path)?; 21 22 // If the system path already exists 23 if symlink_metadata(&system_path).is_ok() { 24 // Check if it is a symlink that points to the correct location 25 if let Ok(destination) = fs::read_link(&system_path) 26 && destination == config_path 27 { 28 return Ok(()); 29 } 30 31 // -> It isnt 32 ask_for_overwrite(force, &config_path, &system_path)?; 33 } 34 35 // At this point the path either doesn't exist yet, or the user has decided to overwrite it 36 println!( 37 "Symlinking {} to {}", 38 config_path.display(), 39 system_path.display(), 40 ); 41 create_symlink(&config_path, &system_path) 42 } 43 44 /// Symlink a the given path to its location in the actual system 45 pub fn add_copy(path: &Path, force: bool) -> Result<()> { 46 let config_path = config_path(path)?; 47 let system_path = system_path(path)?; 48 49 ensure!( 50 !config_path.is_dir(), 51 "Only files and symlinks are currently supported with --copy" 52 ); 53 54 // If path exists on the system 55 if fs::exists(path).with_context( 56 || format!("checking if the path {} already exists", path.display()), 57 // And is not equal to the one in the config 58 )? && let Err(e) = paths_equal(&config_path, &system_path) 59 { 60 eprintln!("{e}"); 61 ask_for_overwrite(force, &config_path, &system_path)?; 62 } 63 64 // At this point the path either doesn't exist yet, or the user has decided to overwrite it 65 println!( 66 "Copying {} to {}", 67 config_path.display(), 68 system_path.display(), 69 ); 70 71 fs::copy(config_path, system_path).map(|_| {}).context( 72 // Ignore number of bytes copied 73 "copying config path to system path", 74 ) 75 } 76 77 /// Asks for overwrite and removes the path from the system if requested, exits if not 78 fn ask_for_overwrite(force: bool, config_path: &Path, system_path: &Path) -> Result<()> { 79 if force 80 || match paths_equal(config_path, system_path) { 81 Ok(()) => true, 82 Err(e) => { 83 bool_question(&format!("{e}, overwrite system path?")).unwrap_or_default() 84 && bool_question("Are you sure?").unwrap_or_default() 85 } 86 } 87 { 88 if system_path.is_dir() { 89 fs::remove_dir_all(system_path) 90 } else { 91 fs::remove_file(system_path) 92 } 93 .with_context(|| format!("removing path {}", system_path.display())) 94 } else { 95 exit(1) 96 } 97 } 98 99 /// Creates a symlink from `config_path` to `system_path` 100 #[expect(clippy::wildcard_enum_match_arm)] 101 fn create_symlink(config_path: &Path, system_path: &Path) -> Result<()> { 102 // Try creating the symlink 103 if let Err(e) = symlink(config_path, system_path) { 104 match e.kind() { 105 ErrorKind::PermissionDenied => { 106 rerun_with_root("Creating symlink"); 107 } 108 ErrorKind::NotFound => { 109 create_dir_all( 110 system_path 111 .parent() 112 .context("Failed to get parent of system path")?, 113 ) 114 .context("creating parent directories")?; 115 116 create_symlink(config_path, system_path)?; 117 } 118 _ => { 119 return Err(e.into()); 120 } 121 } 122 } 123 Ok(()) 124 } 125 126 /// Asks the user the given question and returns the users answer. 127 pub fn bool_question(question: &str) -> Result<bool> { 128 print!("{question} "); 129 130 stdout().flush()?; 131 132 let mut buffer = String::with_capacity(3); // The longest accepted answer is 3 characters long 133 134 loop { 135 buffer.clear(); 136 137 stdin().read_line(&mut buffer)?; 138 139 match buffer.trim() { 140 "y" | "Y" | "yes" | "Yes" => return Ok(true), 141 "n" | "N" | "no" | "No" => return Ok(false), 142 _other => {} 143 } 144 } 145 }