1use crate::scanner::FileAction;
2use anyhow::{Context, Result};
3use std::os::unix::fs::PermissionsExt;
4use std::path::Path;
5
6#[derive(Debug)]
7pub enum DeployResult {
8 Created,
9 Updated,
10 Unchanged,
11 Conflict(String),
12 DryRun,
13}
14
15pub fn deploy_symlink(
20 action: &FileAction,
21 target_dir: &Path,
22 dry_run: bool,
23 force: bool,
24) -> Result<DeployResult> {
25 let target_path = target_dir.join(&action.target_rel_path);
26
27 if dry_run {
28 return Ok(DeployResult::DryRun);
29 }
30
31 let was_existing = target_path.is_symlink() || target_path.exists();
32
33 if target_path.is_dir() && !target_path.is_symlink() {
35 return Ok(DeployResult::Conflict(format!(
36 "target is a directory (remove it manually): {}",
37 target_path.display()
38 )));
39 }
40
41 if target_path.is_symlink() {
43 std::fs::remove_file(&target_path)
44 .with_context(|| format!("failed to remove existing symlink: {}", target_path.display()))?;
45 } else if target_path.exists() {
46 if force {
48 std::fs::remove_file(&target_path)
49 .with_context(|| format!("failed to remove existing file: {}", target_path.display()))?;
50 } else {
51 return Ok(DeployResult::Conflict(format!(
52 "file already exists and is not managed by dotm: {}",
53 target_path.display()
54 )));
55 }
56 }
57
58 if let Some(parent) = target_path.parent() {
60 std::fs::create_dir_all(parent)
61 .with_context(|| format!("failed to create target directory: {}", parent.display()))?;
62 }
63
64 let abs_source = std::fs::canonicalize(&action.source)
66 .with_context(|| format!("failed to canonicalize source path: {}", action.source.display()))?;
67 std::os::unix::fs::symlink(&abs_source, &target_path)
68 .with_context(|| format!("failed to create symlink: {} -> {}", target_path.display(), abs_source.display()))?;
69
70 if was_existing {
71 Ok(DeployResult::Updated)
72 } else {
73 Ok(DeployResult::Created)
74 }
75}
76
77pub fn deploy_copy(
82 action: &FileAction,
83 target_dir: &Path,
84 dry_run: bool,
85 force: bool,
86 rendered_content: Option<&str>,
87) -> Result<DeployResult> {
88 let target_path = target_dir.join(&action.target_rel_path);
89
90 if dry_run {
91 return Ok(DeployResult::DryRun);
92 }
93
94 let was_existing = target_path.is_symlink() || target_path.exists();
95
96 if target_path.is_dir() && !target_path.is_symlink() {
98 return Ok(DeployResult::Conflict(format!(
99 "target is a directory (remove it manually): {}",
100 target_path.display()
101 )));
102 }
103
104 if target_path.exists() || target_path.is_symlink() {
105 if target_path.is_symlink() {
106 std::fs::remove_file(&target_path)
107 .with_context(|| format!("failed to remove existing symlink: {}", target_path.display()))?;
108 } else if force {
109 std::fs::remove_file(&target_path)
110 .with_context(|| format!("failed to remove existing file: {}", target_path.display()))?;
111 } else {
112 return Ok(DeployResult::Conflict(format!(
113 "file already exists and is not managed by dotm: {}",
114 target_path.display()
115 )));
116 }
117 }
118
119 if let Some(parent) = target_path.parent() {
121 std::fs::create_dir_all(parent)
122 .with_context(|| format!("failed to create directory: {}", parent.display()))?;
123 }
124
125 match action.kind {
126 crate::scanner::EntryKind::Template => {
127 let content = rendered_content.unwrap_or("");
128 std::fs::write(&target_path, content)
129 .with_context(|| format!("failed to write template output: {}", target_path.display()))?;
130 }
131 crate::scanner::EntryKind::Base | crate::scanner::EntryKind::Override => {
132 std::fs::copy(&action.source, &target_path)
133 .with_context(|| format!("failed to copy {} to {}", action.source.display(), target_path.display()))?;
134 copy_permissions(&action.source, &target_path)?;
135 }
136 }
137
138 if was_existing {
139 Ok(DeployResult::Updated)
140 } else {
141 Ok(DeployResult::Created)
142 }
143}
144
145pub fn apply_permission_override(path: &Path, mode_str: &str) -> Result<()> {
147 let mode = u32::from_str_radix(mode_str, 8)
148 .with_context(|| format!("invalid octal permission string: '{mode_str}'"))?;
149 let permissions = std::fs::Permissions::from_mode(mode);
150 std::fs::set_permissions(path, permissions)
151 .with_context(|| format!("failed to set permissions {mode_str} on {}", path.display()))?;
152 Ok(())
153}
154
155fn copy_permissions(source: &Path, dest: &Path) -> Result<()> {
157 let metadata = std::fs::metadata(source)
158 .with_context(|| format!("failed to read metadata from {}", source.display()))?;
159 std::fs::set_permissions(dest, metadata.permissions())
160 .with_context(|| format!("failed to set permissions on {}", dest.display()))?;
161 Ok(())
162}