1use crate::config::{Condition, ConflictStrategy, Directive, DirectiveStep, Os, StateConfig};
2use crate::LinkDirectoryBehaviour;
3use anyhow::{anyhow, Context, Result};
4use fs_extra::dir::CopyOptions;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use tera::{Context as TeraContext, Tera};
8
9pub trait OsDetector {
10 fn get_os(&self) -> Result<Os>;
11}
12
13pub struct RealOsDetector;
14
15impl OsDetector for RealOsDetector {
16 fn get_os(&self) -> Result<Os> {
17 Ok(Os::Linux)
18 }
19}
20
21pub struct Executor<T>
22where
23 T: OsDetector,
24{
25 pub dry_run: bool,
26 pub shell: String,
27 pub os_detector: T,
28 pub conflict_strategy: ConflictStrategy,
29}
30
31impl Executor<RealOsDetector> {
32 pub fn dry_run(shell: &str, conflict_strategy: ConflictStrategy) -> Self {
33 Self::create(true, shell, conflict_strategy)
34 }
35
36 pub fn new(shell: &str, conflict_strategy: ConflictStrategy) -> Self {
37 Self::create(false, shell, conflict_strategy)
38 }
39
40 fn create(dry_run: bool, shell: &str, conflict_strategy: ConflictStrategy) -> Self {
41 Self {
42 shell: shell.to_string(),
43 dry_run,
44 os_detector: RealOsDetector,
45 conflict_strategy,
46 }
47 }
48}
49
50impl<T> Executor<T>
51where
52 T: OsDetector,
53{
54 pub fn execute<P: AsRef<Path>>(&self, root_dir: P, section: &str, directives: &[DirectiveStep]) -> Result<()> {
55 let root_dir = root_dir.as_ref();
56 if self.dry_run {
57 info!("Using root_dir: {}", root_dir.display());
58 } else {
59 debug!("Using root_dir: {}", root_dir.display());
60 }
61
62 for directive in directives {
63 debug!("Executing [section={}] [directive={:?}]", section, directive);
64 self.execute_directive(root_dir, directive)?;
65 }
66
67 info!("Executed section {}", section);
68 Ok(())
69 }
70
71 fn execute_directive(&self, root_dir: &Path, directive: &DirectiveStep) -> Result<()> {
72 match &directive.condition {
73 Condition::Always => debug!("Directive has no condition. Executing"),
74 Condition::IfOs(os) => {
75 let current_os = self.os_detector.get_os().context("Error detecting current OS")?;
76 if ¤t_os == os {
77 debug!("OS match. Executing");
78 } else {
79 debug!("OS does not match. Not executing");
80 return Ok(());
81 }
82 }
83 }
84
85 match &directive.directive {
86 Directive::Link {
87 from,
88 to,
89 directory_behaviour,
90 } => {
91 debug!(
92 "Link directive [from={}] [to={}] [behaviour={}]",
93 from,
94 to,
95 directory_behaviour.to_string()
96 );
97 self.execute_symlink(root_dir, from, to, directory_behaviour)?;
98 }
99 Directive::Copy { from, to } => {
100 debug!("Copy directive [from={}] [to={}]", from, to);
101 self.execute_copy(root_dir, from, to)?;
102 }
103 Directive::Run(cmd) => {
104 debug!("Run directive [cmd={}]", cmd);
105 self.run(root_dir, cmd)?;
106 }
107 Directive::Include(path) => {
108 debug!("Include directive [path={}]", path);
109 self.include(root_dir, path)?;
110 }
111 Directive::Template { template, dest, vars } => {
112 debug!("Template directive [template={}] [dest={}] [vars={:?}]", template, dest, vars);
113 self.template(root_dir, template, dest, vars)?;
114 }
115 }
116
117 Ok(())
118 }
119
120 fn execute_symlink(&self, root_dir: &Path, from: &str, to: &str, behaviour: &LinkDirectoryBehaviour) -> Result<()> {
121 let paths = self
122 .get_paths_to_process(root_dir, from, to)
123 .context("Error obtaining paths to process")?;
124 let remove_dirs = behaviour.ne(&LinkDirectoryBehaviour::CreateDirectory);
125 for (from, to) in paths {
126 let (from_path, to_path) = self
127 .check_for_conflicts(root_dir, &from, &to, remove_dirs)
128 .context("Error in symlink prerequirements")?;
129 if from_path.is_dir() {
130 match behaviour {
131 LinkDirectoryBehaviour::IgnoreDirectories => {
132 if self.dry_run {
133 info!(
134 "Skipping dir {} as LinkDirectoryBehaviour is set to IgnoreDirectories",
135 from_path.display()
136 );
137 } else {
138 debug!(
139 "Skipping dir {} as LinkDirectoryBehaviour is set to IgnoreDirectories",
140 from_path.display()
141 );
142 }
143 }
144 LinkDirectoryBehaviour::LinkDirectory => {
145 if self.dry_run {
146 info!("Would symlink dir {} -> {}", from_path.display(), to_path.display());
147 } else {
148 symlink::symlink_dir(&from_path, &to_path).context(format!(
149 "Error symlinking dir {} -> {}",
150 from_path.display(),
151 to_path.display()
152 ))?;
153 info!("Symlinked dir {} -> {}", from_path.display(), to_path.display());
154 }
155 }
156 LinkDirectoryBehaviour::CreateDirectory => {
157 if !to_path.exists() {
158 if self.dry_run {
159 info!(
160 "Would create dir {} as LinkDirectoryBehaviour is set to CreateDirectory",
161 to_path.display()
162 );
163 } else {
164 debug!(
165 "Creating dir {} as LinkDirectoryBehaviour is set to CreateDirectory",
166 to_path.display()
167 );
168 std::fs::create_dir(&to_path).context(format!("Error creating directory {}", to_path.display()))?;
169 info!("Created dir {}", to_path.display());
170 }
171 } else if self.dry_run {
172 info!("To path already exists, no need to do anything {}", to_path.display());
173 } else {
174 debug!("To path already exists, no need to do anything {}", to_path.display());
175 }
176
177 let from_files =
179 std::fs::read_dir(&from_path).context(format!("Error getting dir contents of {}", from_path.display()))?;
180 for entry in from_files {
181 let entry = entry.context(format!("Error getting entry of dir {}", from_path.display()))?;
182 let entry = entry.path();
183 let entry_without_prefix = entry
184 .strip_prefix(&root_dir)
185 .context(format!(
186 "Error stripping prefix from entry [entry={}] [prefix={}]",
187 entry.display(),
188 root_dir.display()
189 ))?
190 .to_path_buf();
191 let from_path = entry_without_prefix.display().to_string();
192 let from_filename = match entry.file_name() {
193 Some(f) => match f.to_str() {
194 Some(filename) => filename.to_string(),
195 None => return Err(anyhow!("Cannot convert to str {:?}", f)),
196 },
197 None => return Err(anyhow!("Cannot obtain filename from {}", entry.display())),
198 };
199 let to_path = format!("{}/{}", to, from_filename);
200 self.execute_symlink(root_dir, &from_path, &to_path, behaviour)?;
201 }
202 }
203 }
204 } else if self.dry_run {
205 info!("Would symlink file {} -> {}", from_path.display(), to_path.display());
206 } else {
207 symlink::symlink_file(&from_path, &to_path).context(format!(
208 "Error symlinking file {} -> {}",
209 from_path.display(),
210 to_path.display()
211 ))?;
212 info!("Symlinked file {} -> {}", from_path.display(), to_path.display());
213 }
214 }
215
216 Ok(())
217 }
218
219 fn execute_copy(&self, root_dir: &Path, from: &str, to: &str) -> Result<()> {
220 let paths = self
221 .get_paths_to_process(root_dir, from, to)
222 .context("Error obtaining paths to process")?;
223 for (from, to) in paths {
224 let (from, to) = self
225 .check_for_conflicts(root_dir, &from, &to, true)
226 .context("Error in copy prerequirements")?;
227 if from.is_dir() {
228 if self.dry_run {
229 info!("Would copy dir {} -> {}", from.display(), to.display());
230 } else {
231 fs_extra::dir::copy(
232 &from,
233 &to,
234 &CopyOptions {
235 overwrite: true,
236 ..CopyOptions::default()
237 },
238 )
239 .context(format!("Error copying dir {} -> {}", from.display(), to.display()))?;
240 info!("Copied dir {} -> {}", from.display(), to.display());
241 }
242 } else if self.dry_run {
243 info!("Would copy file {} -> {}", from.display(), to.display());
244 } else {
245 debug!("Copying {} -> {}", from.display(), to.display());
246 std::fs::copy(&from, &to).context(format!("Error copying file {} -> {}", from.display(), to.display()))?;
247 info!("Copied file {} -> {}", from.display(), to.display());
248 }
249 }
250
251 Ok(())
252 }
253
254 fn get_paths_to_process(&self, root_dir: &Path, from: &str, to: &str) -> Result<Vec<(String, String)>> {
255 let mut paths = vec![];
256 let to_dest = if to.contains('~') {
257 PathBuf::from(shellexpand::tilde(to).to_string())
258 } else {
259 root_dir.join(to)
260 };
261 if !is_glob(from) {
262 paths.push((from.to_string(), to_dest.display().to_string()));
263 } else {
264 debug!("Detected from is glob: {}", from);
265 if !to_dest.exists() {
266 if self.dry_run {
268 info!("Would have created dir {}", to_dest.display());
269 } else {
270 debug!("Creating dir {}", to_dest.display());
271 std::fs::create_dir_all(&to_dest).context(format!("Error creating directory {}", to_dest.display()))?;
272 }
273 } else if !to_dest.is_dir() {
274 return Err(anyhow!("Asked to copy into a path that is not a directory"));
275 }
276
277 let full_glob = root_dir.join(from).display().to_string();
278 debug!("Detected from is glob {} | Will use {}", from, full_glob);
279 let glob_iter = glob::glob(&full_glob).context(format!("Error obtaining iterator from glob {}", full_glob))?;
280 for entry in glob_iter {
281 let entry = entry.context("Error obtaining glob entry")?;
282 let entry_without_prefix = entry
283 .strip_prefix(&root_dir)
284 .context("Error stripping prefix from glob")?
285 .to_path_buf();
286 let from_path = entry_without_prefix.display().to_string();
287 let from_filename = match entry.file_name() {
288 Some(f) => match f.to_str() {
289 Some(filename) => filename.to_string(),
290 None => return Err(anyhow!("Cannot convert to str {:?}", f)),
291 },
292 None => return Err(anyhow!("Cannot obtain filename from {}", entry.display())),
293 };
294 let to_path = format!("{}/{}", to, from_filename);
295 paths.push((from_path, to_path));
296 }
297 }
298 Ok(paths)
299 }
300
301 fn check_for_conflicts(&self, root_dir: &Path, from: &str, to: &str, delete_if_dir: bool) -> Result<(PathBuf, PathBuf)> {
302 let from_path = root_dir.join(from);
304 let to_path = if to.contains('~') {
305 PathBuf::from(shellexpand::tilde(to).to_string())
306 } else {
307 root_dir.join(to)
308 };
309 debug!("Checking if 'from' exists: {}", from_path.display());
310
311 if !from_path.exists() {
312 return Err(anyhow!("From does not exist: {}", from_path.display()));
313 }
314
315 debug!("Checking if 'to' exists: {}", to_path.display());
317 let mut to_already_exists = to_path.exists();
318 if !to_already_exists {
319 debug!(
320 "Detected 'to' does not exist. Checking if is a broken symlink {}",
321 to_path.display()
322 );
323 if std::fs::symlink_metadata(&to_path).is_ok() {
325 debug!("Detected 'to' is a broken symlink {}", to_path.display());
326 to_already_exists = true;
327 }
328 }
329
330 if to_already_exists {
331 debug!("'to' exists: {}", to_path.display());
332
333 match &self.conflict_strategy {
335 ConflictStrategy::Abort => {
336 warn!("ConflictStrategy set to abort. Aborting");
337 return Err(anyhow!(
338 "'to' {} already exists and ConflictStrategy is set to abort",
339 to_path.display()
340 ));
341 }
342 ConflictStrategy::RenameOld => {
343 debug!("ConflictStrategy set to rename-old. Renaming old");
344
345 let mut counter = 0;
346 let backup_path = loop {
347 let mut to_path_clone = to_path.display().to_string();
348 let suffix = if counter == 0 {
349 ".bak".to_string()
350 } else {
351 format!(".bak{}", counter)
352 };
353 to_path_clone.push_str(&suffix);
354
355 let to_path_bak = Path::new(&to_path_clone);
356 debug!("Checking if backup already exists");
357 if !to_path_bak.exists() {
358 break to_path_clone;
359 }
360 counter += 1;
361 };
362
363 let backup_path = Path::new(&backup_path);
364 if self.dry_run {
365 info!("Would move [src={}] [dst={}]", to_path.display(), backup_path.display());
366 } else {
367 warn!("Moving [src={}] -> [dst={}]", to_path.display(), backup_path.display());
368 std::fs::rename(&to_path, backup_path).context(format!(
369 "Error renaming [src={}] -> [dst={}]",
370 to_path.display(),
371 backup_path.display()
372 ))?;
373 }
374
375 return Ok((from_path, to_path));
376 }
377 ConflictStrategy::Overwrite => {
378 if to_path.is_symlink() {
379 if to_path.is_file() {
380 if self.dry_run {
381 info!("Would remove file symlink {}", to_path.display());
382 } else {
383 warn!("Removing file symlink {}", to_path.display());
384 symlink::remove_symlink_file(&to_path)
385 .context(format!("Error removing file symlink {}", to_path.display()))?;
386 }
387 } else if to_path.is_dir() {
388 if delete_if_dir {
389 if self.dry_run {
390 info!("Would remove dir symlink {}", to_path.display());
391 } else {
392 warn!("Removing dir symlink {}", to_path.display());
393 symlink::remove_symlink_dir(&to_path)
394 .context(format!("Error removing dir symlink {}", to_path.display()))?;
395 }
396 } else if self.dry_run {
397 info!(
398 "Would not remove dir symlink as is specified in configuration {}",
399 to_path.display()
400 );
401 } else {
402 debug!("Not removing dir symlink as is specified in configuration {}", to_path.display());
403 }
404 } else {
405 if self.dry_run {
407 info!("Would remove broken symlink {}", to_path.display());
408 } else {
409 warn!("Removing broken symlink {}", to_path.display());
410 symlink::remove_symlink_file(&to_path).context("Error removing broken symlink")?;
411 }
412 }
413 } else if to_path.is_file() {
414 if self.dry_run {
415 info!("Would remove file {}", to_path.display());
416 } else {
417 warn!("Removing file {}", to_path.display());
418 std::fs::remove_file(&to_path).context(format!("Error removing file {}", to_path.display()))?;
419 }
420 } else if to_path.is_dir() {
421 if delete_if_dir {
422 if self.dry_run {
423 info!("Would remove dir {}", to_path.display());
424 } else {
425 warn!("Removing dir {}", to_path.display());
426 std::fs::remove_dir_all(&to_path).context(format!("Error removing dir {}", to_path.display()))?;
427 }
428 } else if self.dry_run {
429 info!("Would not remove dir as is specified in configuration {}", to_path.display());
430 } else {
431 debug!("Not removing dir as is specified in configuration {}", to_path.display());
432 }
433 } else if self.dry_run {
434 info!("Would remove dir {}", to_path.display());
435 } else {
436 warn!("Removing dir {}", to_path.display());
437 std::fs::remove_dir_all(&to_path).context(format!("Error removing dir {}", to_path.display()))?;
438 }
439 }
440 }
441 } else {
442 debug!("To {} does not exist", to_path.display());
443 if let Some(parent) = to_path.parent() {
445 if !parent.exists() {
446 if self.dry_run {
447 info!("As parent dir does not exist, would have created {}", parent.display());
448 } else {
449 debug!("Creating parent dir structure {}", parent.display());
450 std::fs::create_dir_all(&parent).context(format!("Error creating parent dir structure {}", parent.display()))?;
451 }
452 }
453 }
454 }
455
456 Ok((from_path, to_path))
457 }
458
459 fn run(&self, root_dir: &Path, cmd: &str) -> Result<()> {
460 let current_dir = Path::new(root_dir);
461 let shell_args = self.shell.split(' ').collect::<Vec<&str>>();
462 if shell_args.is_empty() {
463 return Err(anyhow!("Cannot run commands with an empty shell definition"));
464 }
465
466 let mut command = Command::new(shell_args[0]);
467 for arg in shell_args.iter().skip(1) {
468 command.arg(arg);
469 }
470 command.arg(cmd).current_dir(current_dir);
471
472 if self.dry_run {
473 info!(
474 "Would run [current_dir={}]: {:?} {:?}",
475 root_dir.display(),
476 command.get_program(),
477 command.get_args()
478 );
479 } else {
480 debug!("Command to be executed: {:?} {:?}", command.get_program(), command.get_args());
481 let mut exec = command.spawn().context("Error invoking subcommand")?;
482 let exit_status = exec.wait().context("Error waiting for subcommand to finish")?;
483 if let Some(code) = exit_status.code() {
484 debug!("Command exit status: {}", code);
485 if code != 0 {
486 return Err(anyhow!(
487 "Command exit status was not 0. Exit status: {} | Command: {:?} {:?}",
488 exit_status,
489 command.get_program(),
490 command.get_args()
491 ));
492 }
493 }
494
495 info!("Executed command {}", cmd);
496 }
497
498 Ok(())
499 }
500
501 fn include(&self, root_dir: &Path, path: &str) -> Result<()> {
502 let yaml_path = root_dir.join(path);
503 if !yaml_path.exists() {
504 return Err(anyhow!("Could not file yaml to include in path {}", yaml_path.display()));
505 }
506
507 let contents = std::fs::read_to_string(&yaml_path).context(format!("Error loading included file {}", yaml_path.display()))?;
508 let config = StateConfig::from_yaml(&contents).context(format!("Error parsing included file {}", yaml_path.display()))?;
509
510 let included_root_dir = match yaml_path.parent() {
511 Some(p) => p,
512 None => root_dir,
513 };
514 debug!("Using root_dir: {}", included_root_dir.display());
515
516 for (section, directives) in config.states {
517 self.execute(included_root_dir, §ion, &directives)
518 .context(format!("Error executing directives from file {}", yaml_path.display()))?;
519 }
520
521 info!("Finished include section {}", path);
522
523 Ok(())
524 }
525
526 fn template(&self, root_dir: &Path, template: &str, dest: &str, vars: &Option<String>) -> Result<()> {
527 let (template, dest) = self
528 .check_for_conflicts(root_dir, template, dest, true)
529 .context("Error preparing files for templating")?;
530
531 let template_contents =
532 std::fs::read_to_string(&template).context(format!("Error reading template contents: {}", template.display()))?;
533 let mut tera = Tera::default();
534 let mut context = TeraContext::new();
535 let os = self.os_detector.get_os().context("Error detecting current os")?;
537 context.insert("dotfilers_os", &os.to_string());
538 load_vars_into_context(root_dir, vars, &mut context).context("Error loading template vars")?;
539
540 let rendered = tera.render_str(&template_contents, &context).context("Error rendering template")?;
541
542 if self.dry_run {
543 info!("Would have written into {} the following template: {}", dest.display(), rendered);
544 } else {
545 debug!("Writing template into {}", dest.display());
546 std::fs::write(&dest, rendered).context(format!("Error writing templated contents into {}", dest.display()))?;
547 info!("Rendered file {}", dest.display());
548 }
549
550 Ok(())
551 }
552}
553
554fn is_glob(path: &str) -> bool {
555 path.contains('*')
556}
557
558fn load_vars_into_context(root_dir: &Path, vars: &Option<String>, context: &mut TeraContext) -> Result<()> {
559 let vars = match vars {
560 Some(v) => v,
561 None => return Ok(()),
562 };
563
564 let vars_path = root_dir.join(vars);
565
566 if !vars_path.exists() {
567 return Err(anyhow!("Could not find vars file {}", vars_path.display()));
568 }
569
570 if !vars_path.is_file() {
571 return Err(anyhow!("Vars file is not a file {}", vars_path.display()));
572 }
573
574 let vars_contents = std::fs::read_to_string(&vars_path).context(format!("Error reading vars file {}", vars_path.display()))?;
575 load_vars_into_context_from_str(&vars_contents, context);
576
577 Ok(())
578}
579
580fn load_vars_into_context_from_str(contents: &str, context: &mut TeraContext) {
581 for line in contents.lines() {
582 if line.is_empty() || line.starts_with('#') {
583 continue;
584 }
585 let splits = line.split_once('=');
586 if let Some((name, value)) = splits {
587 context.insert(name.to_string(), &value.to_string());
588 }
589 }
590}
591
592#[cfg(test)]
593mod test {
594 use super::*;
595
596 #[test]
597 fn load_vars_into_context() {
598 let mut context = TeraContext::new();
599 load_vars_into_context_from_str(
600 r#"
601name=test
602number=123
603withequals=a=b=c
604# contents=abc
605 "#,
606 &mut context,
607 );
608
609 assert_eq!(context.get("name"), Some(&tera::Value::String("test".to_string())));
610 assert_eq!(context.get("number"), Some(&tera::Value::String("123".to_string())));
611 assert_eq!(context.get("withequals"), Some(&tera::Value::String("a=b=c".to_string())));
612
613 let as_json = context.into_json();
615 let v = as_json.as_object().unwrap();
616 assert_eq!(v.len(), 3);
617 }
618}