lib.rs
1 use std::collections::HashSet; 2 use std::ffi::OsStr; 3 use std::fs::{self}; 4 use std::io::{self}; 5 use std::path::{Path, PathBuf}; 6 7 use tracing::{debug, info, trace, warn}; 8 use walkdir::WalkDir; 9 10 pub struct Dotr { 11 ignore: HashSet<PathBuf>, 12 13 dry_run: bool, 14 force: bool, 15 } 16 17 impl Dotr { 18 pub fn new() -> Self { 19 Dotr { 20 ignore: HashSet::new(), 21 dry_run: false, 22 force: false, 23 } 24 } 25 26 pub fn set_force(self) -> Self { 27 Self { 28 force: true, 29 ..self 30 } 31 } 32 33 pub fn set_dry_run(self) -> Self { 34 Self { 35 dry_run: true, 36 ..self 37 } 38 } 39 40 pub fn link_entry( 41 &self, 42 src: &walkdir::DirEntry, 43 src_base: &Path, 44 dst_base: &Path, 45 ) -> io::Result<()> { 46 trace!(path = %src.path().display(), "Walking path"); 47 48 let src = src.path(); 49 let src_rel = src.strip_prefix(src_base).unwrap(); 50 51 if self.ignore.contains(src_rel) { 52 debug!(path = %src.display(), "Ignoring file"); 53 return Ok(()); 54 } 55 56 let dst = dst_base.join(src_rel); 57 let dst_metadata = dst.symlink_metadata().ok(); 58 let dst_type = dst_metadata.map(|m| m.file_type()); 59 60 let src_metadata = src.symlink_metadata()?; 61 let src_type = src_metadata.file_type(); 62 63 if src_type.is_dir() { 64 return Ok(()); 65 } else if src_type.is_file() { 66 trace!(src = %src.display(), dst=%dst.display(), "Source is a file"); 67 if dst.exists() || dst.symlink_metadata().is_ok() { 68 if self.force { 69 if dst_type.is_some_and(|t| t.is_dir()) { 70 io::Error::new( 71 io::ErrorKind::Other, 72 format!("Can't safely remove {} as it's a directory", dst.display()), 73 ); 74 } 75 if !self.dry_run { 76 debug!(src = %src.display(), dst=%dst.display(), "Force removing destination"); 77 fs::remove_file(&dst)?; 78 } else { 79 debug!(src = %src.display(), dst=%dst.display(), "Force removing destination (dry-run)"); 80 } 81 } else { 82 if dst_type.map(|t| t.is_symlink()).unwrap_or(false) { 83 let dst_link_dst = dst.read_link()?; 84 if *dst_link_dst == *src { 85 debug!(src = %src.display(), dst=%dst.display(), "Destination already points to the source"); 86 return Ok(()); 87 } else { 88 warn!(src = %src.display(), dst = %dst.display(), dst_dst = %dst_link_dst.display(), "Destination already exists and points elsewhere"); 89 } 90 } else { 91 warn!(src = %src.display(), dst=%dst.display(), "Destination already exists and is not a symlink"); 92 } 93 return Ok(()); 94 } 95 } else if !self.dry_run { 96 trace!(src = %src.display(), dst=%dst.display(), "Creating a base directory (if doesn't exist)"); 97 fs::create_dir_all(dst.parent().unwrap())?; 98 } 99 100 if !self.dry_run { 101 trace!(src = %src.display(), dst=%dst.display(), "Creating symlink to a src file"); 102 std::os::unix::fs::symlink(src, &dst)?; 103 } 104 } else if src_type.is_symlink() { 105 let src_link = src.read_link()?; 106 trace!(src = %src.display(), dst=%dst.display(), "src-link" = %src_link.display(), "Source is a symlink"); 107 if dst.exists() || dst.symlink_metadata().is_ok() { 108 if self.force { 109 if !self.dry_run { 110 debug!(src = %src.display(), dst = %dst.display(), "Force removing destination"); 111 fs::remove_file(&dst)?; 112 } else { 113 debug!(src = %src.display(), dst = %dst.display(), "Force removing destination (dry-run)"); 114 } 115 } else if Some(src_link.clone()) == dst.read_link().ok() { 116 debug!( 117 src = %src.display(), dst = %dst.display(), 118 "Destination already points to the source (symlink source)" 119 ); 120 return Ok(()); 121 } else { 122 warn!(src = %src.display(), dst = %dst.display(), "Destination already exists"); 123 return Ok(()); 124 } 125 } else if !self.dry_run { 126 trace!(src = %src.display(), dst = %dst.display(), "Creating a base directory (if doesn't exist)"); 127 fs::create_dir_all(dst.parent().unwrap())?; 128 } 129 if !self.dry_run { 130 trace!(src = %src.display(), dst = %dst.display(), "src-link" = %src_link.display(), "Duplicating symlink"); 131 std::os::unix::fs::symlink(&src_link, &dst)?; 132 } 133 } else { 134 warn!(src = %src.display(), dst = %dst.display(), "Skipping unknown source file type"); 135 } 136 Ok(()) 137 } 138 139 pub fn link(&self, src_base: &Path, dst_base: &Path) -> io::Result<()> { 140 info!(src = %src_base.display(), dst = %dst_base.display(), "Starting link operation"); 141 142 if !dst_base.exists() { 143 return Err(io::Error::new( 144 io::ErrorKind::NotFound, 145 "Destination doesn't exist", 146 )); 147 } 148 149 if !dst_base.is_dir() { 150 return Err(io::Error::new( 151 io::ErrorKind::AlreadyExists, 152 "Destination is not a directory", 153 )); 154 } 155 156 let dst_base = dst_base.canonicalize()?; 157 let src_base = src_base.canonicalize()?; 158 159 assert!(dst_base.is_absolute()); 160 assert!(src_base.is_absolute()); 161 162 for src in WalkDir::new(&src_base) 163 .into_iter() 164 .filter_entry(should_traverse) 165 .filter_map(|e| e.ok()) 166 { 167 self.link_entry(&src, &src_base, &dst_base)?; 168 } 169 170 Ok(()) 171 } 172 173 pub fn unlink(&self, src_base: &Path, dst_base: &Path) -> io::Result<()> { 174 info!(src = %src_base.display(), dst = %dst_base.display(), "Starting unlink operation"); 175 176 let dst_base = dst_base.canonicalize()?; 177 let src_base = src_base.canonicalize()?; 178 179 assert!(dst_base.is_absolute()); 180 assert!(src_base.is_absolute()); 181 182 for src in WalkDir::new(&src_base) 183 .into_iter() 184 .filter_entry(should_traverse) 185 .filter_map(|e| e.ok()) 186 { 187 self.unlink_entry(&src, &src_base, &dst_base)?; 188 } 189 190 Ok(()) 191 } 192 193 pub fn unlink_entry( 194 &self, 195 src: &walkdir::DirEntry, 196 src_base: &Path, 197 dst_base: &Path, 198 ) -> io::Result<()> { 199 trace!(path = %src.path().display(), "Walking path"); 200 201 let src = src.path(); 202 let src_rel = src.strip_prefix(src_base).unwrap(); 203 204 if self.ignore.contains(src_rel) { 205 debug!(path = %src.display(), "Ignoring file"); 206 return Ok(()); 207 } 208 209 let dst = dst_base.join(src_rel); 210 211 let src_metadata = src.symlink_metadata()?; 212 let src_type = src_metadata.file_type(); 213 214 if src_type.is_dir() { 215 return Ok(()); 216 } else if src_type.is_file() { 217 trace!(src = %src.display(), dst = %dst.display(), "Unlink a file"); 218 let dst_metadata = dst.symlink_metadata(); 219 // exists follows symlinks :/ 220 if dst.exists() || dst_metadata.is_ok() { 221 let dst_metadata = dst_metadata?; 222 if self.force { 223 if !self.dry_run { 224 debug!(src = %src.display(), dst = %dst.display(), "Force removing"); 225 fs::remove_file(&dst)?; 226 return Ok(()); 227 } else { 228 debug!(src = %src.display(), dst = %dst.display(), "Force removing (dry run)"); 229 } 230 } else if dst_metadata.file_type().is_file() { 231 warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a file"); 232 return Ok(()); 233 } else if dst_metadata.file_type().is_dir() { 234 warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a directory"); 235 return Ok(()); 236 } else if dst_metadata.file_type().is_symlink() { 237 let dst_link = dst.read_link()?; 238 if dst_link != src { 239 warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a symlink pointing to something else"); 240 return Ok(()); 241 } else if !self.dry_run { 242 fs::remove_file(&dst)?; 243 } 244 } else { 245 warn!(src = %src.display(), dst = %dst.display(), "Destination exists and is of unknown file type"); 246 } 247 } else { 248 debug!(src = %src.display(), dst = %dst.display(), "Destination doesn't exist - nothing to unlink"); 249 return Ok(()); 250 } 251 } else if src_type.is_symlink() { 252 let src_link = src.read_link()?; 253 trace!(src = %src.display(), dst = %dst.display(), "Unlink a symlink"); 254 let dst_metadata = dst.symlink_metadata(); 255 // exists follows symlinks :/ 256 if dst.exists() || dst_metadata.is_ok() { 257 let dst_metadata = dst_metadata?; 258 if self.force { 259 if !self.dry_run { 260 fs::remove_file(&dst)?; 261 return Ok(()); 262 } 263 } else if dst_metadata.file_type().is_file() { 264 warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a file"); 265 return Ok(()); 266 } else if dst_metadata.file_type().is_dir() { 267 warn!(src = %src.display(), dst = %dst.display(), "Destination already exists and is a directory"); 268 return Ok(()); 269 } else if dst_metadata.file_type().is_symlink() { 270 let dst_link = dst.read_link()?; 271 if dst_link != src_link { 272 warn!( 273 src = %src.display(), 274 dst = %dst.display(), 275 "dst-link" = %dst_link.display(), 276 "src-link" = %src_link.display(), 277 "Destination already exists and is a symlink pointing to something else", 278 ); 279 return Ok(()); 280 } else if !self.dry_run { 281 fs::remove_file(&dst)?; 282 } 283 } else { 284 warn!(src = %src.display(), dst = %dst.display(), "Destination exists and is of unknown file type"); 285 } 286 } else { 287 debug!(src = %src.display(), dst = %dst.display(), "Destination doesn't exist - nothing to unlink"); 288 return Ok(()); 289 } 290 } else { 291 warn!(src = %src.display(), dst = %dst.display(), "Skipping unknown source file type"); 292 } 293 Ok(()) 294 } 295 } 296 297 impl Default for Dotr { 298 fn default() -> Self { 299 Self::new() 300 } 301 } 302 303 fn should_traverse(de: &walkdir::DirEntry) -> bool { 304 if !de.path().is_dir() { 305 return true; 306 } 307 308 if de.path().file_name() == Some(OsStr::new(".git")) { 309 return false; 310 } 311 312 true 313 }