1use crate::scanner::{EntryKind, 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_staged(
21 action: &FileAction,
22 staging_dir: &Path,
23 target_dir: &Path,
24 dry_run: bool,
25 force: bool,
26 rendered_content: Option<&str>,
27) -> Result<DeployResult> {
28 let staged_path = staging_dir.join(&action.target_rel_path);
29 let target_path = target_dir.join(&action.target_rel_path);
30
31 if dry_run {
32 return Ok(DeployResult::DryRun);
33 }
34
35 let was_existing = target_path.is_symlink() || target_path.exists();
37
38 if target_path.exists() || target_path.is_symlink() {
40 if target_path.is_symlink() {
41 std::fs::remove_file(&target_path)
42 .with_context(|| format!("failed to remove existing symlink: {}", target_path.display()))?;
43 } else if force {
44 std::fs::remove_file(&target_path)
45 .with_context(|| format!("failed to remove existing file: {}", target_path.display()))?;
46 } else {
47 return Ok(DeployResult::Conflict(format!(
48 "file already exists and is not managed by dotm: {}",
49 target_path.display()
50 )));
51 }
52 }
53
54 if let Some(parent) = staged_path.parent() {
56 std::fs::create_dir_all(parent)
57 .with_context(|| format!("failed to create staging directory: {}", parent.display()))?;
58 }
59 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 match action.kind {
66 EntryKind::Template => {
67 let content = rendered_content.unwrap_or("");
68 std::fs::write(&staged_path, content)
69 .with_context(|| format!("failed to write template to staging: {}", staged_path.display()))?;
70 }
71 EntryKind::Base | EntryKind::Override => {
72 std::fs::copy(&action.source, &staged_path)
73 .with_context(|| format!("failed to copy {} to staging: {}", action.source.display(), staged_path.display()))?;
74 copy_permissions(&action.source, &staged_path)?;
75 }
76 }
77
78 let abs_staged = std::fs::canonicalize(&staged_path)
80 .with_context(|| format!("failed to canonicalize staged path: {}", staged_path.display()))?;
81 std::os::unix::fs::symlink(&abs_staged, &target_path)
82 .with_context(|| format!("failed to create symlink: {} -> {}", target_path.display(), abs_staged.display()))?;
83
84 if was_existing {
85 Ok(DeployResult::Updated)
86 } else {
87 Ok(DeployResult::Created)
88 }
89}
90
91pub fn deploy_copy(
96 action: &FileAction,
97 target_dir: &Path,
98 dry_run: bool,
99 force: bool,
100 rendered_content: Option<&str>,
101) -> Result<DeployResult> {
102 let target_path = target_dir.join(&action.target_rel_path);
103
104 if dry_run {
105 return Ok(DeployResult::DryRun);
106 }
107
108 let was_existing = target_path.is_symlink() || target_path.exists();
110
111 if target_path.exists() || target_path.is_symlink() {
113 if target_path.is_symlink() {
114 std::fs::remove_file(&target_path)
115 .with_context(|| format!("failed to remove existing symlink: {}", target_path.display()))?;
116 } else if force {
117 std::fs::remove_file(&target_path)
118 .with_context(|| format!("failed to remove existing file: {}", target_path.display()))?;
119 } else {
120 return Ok(DeployResult::Conflict(format!(
121 "file already exists and is not managed by dotm: {}",
122 target_path.display()
123 )));
124 }
125 }
126
127 if let Some(parent) = target_path.parent() {
129 std::fs::create_dir_all(parent)
130 .with_context(|| format!("failed to create directory: {}", parent.display()))?;
131 }
132
133 match action.kind {
134 EntryKind::Template => {
135 let content = rendered_content.unwrap_or("");
136 std::fs::write(&target_path, content)
137 .with_context(|| format!("failed to write template output: {}", target_path.display()))?;
138 }
139 EntryKind::Base | EntryKind::Override => {
140 std::fs::copy(&action.source, &target_path)
141 .with_context(|| format!("failed to copy {} to {}", action.source.display(), target_path.display()))?;
142 copy_permissions(&action.source, &target_path)?;
143 }
144 }
145
146 if was_existing {
147 Ok(DeployResult::Updated)
148 } else {
149 Ok(DeployResult::Created)
150 }
151}
152
153pub fn apply_permission_override(path: &Path, mode_str: &str) -> Result<()> {
155 let mode = u32::from_str_radix(mode_str, 8)
156 .with_context(|| format!("invalid octal permission string: '{mode_str}'"))?;
157 let permissions = std::fs::Permissions::from_mode(mode);
158 std::fs::set_permissions(path, permissions)
159 .with_context(|| format!("failed to set permissions {mode_str} on {}", path.display()))?;
160 Ok(())
161}
162
163fn copy_permissions(source: &Path, dest: &Path) -> Result<()> {
165 let metadata = std::fs::metadata(source)
166 .with_context(|| format!("failed to read metadata from {}", source.display()))?;
167 std::fs::set_permissions(dest, metadata.permissions())
168 .with_context(|| format!("failed to set permissions on {}", dest.display()))?;
169 Ok(())
170}