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 if target_path.exists() || target_path.is_symlink() {
37 if target_path.is_symlink() {
38 std::fs::remove_file(&target_path)
39 .with_context(|| format!("failed to remove existing symlink: {}", target_path.display()))?;
40 } else if force {
41 std::fs::remove_file(&target_path)
42 .with_context(|| format!("failed to remove existing file: {}", target_path.display()))?;
43 } else {
44 return Ok(DeployResult::Conflict(format!(
45 "file already exists and is not managed by dotm: {}",
46 target_path.display()
47 )));
48 }
49 }
50
51 if let Some(parent) = staged_path.parent() {
53 std::fs::create_dir_all(parent)
54 .with_context(|| format!("failed to create staging directory: {}", parent.display()))?;
55 }
56 if let Some(parent) = target_path.parent() {
57 std::fs::create_dir_all(parent)
58 .with_context(|| format!("failed to create target directory: {}", parent.display()))?;
59 }
60
61 match action.kind {
63 EntryKind::Template => {
64 let content = rendered_content.unwrap_or("");
65 std::fs::write(&staged_path, content)
66 .with_context(|| format!("failed to write template to staging: {}", staged_path.display()))?;
67 }
68 EntryKind::Base | EntryKind::Override => {
69 std::fs::copy(&action.source, &staged_path)
70 .with_context(|| format!("failed to copy {} to staging: {}", action.source.display(), staged_path.display()))?;
71 copy_permissions(&action.source, &staged_path)?;
72 }
73 }
74
75 let abs_staged = std::fs::canonicalize(&staged_path)
77 .with_context(|| format!("failed to canonicalize staged path: {}", staged_path.display()))?;
78 std::os::unix::fs::symlink(&abs_staged, &target_path)
79 .with_context(|| format!("failed to create symlink: {} -> {}", target_path.display(), abs_staged.display()))?;
80
81 Ok(DeployResult::Created)
82}
83
84pub fn deploy_copy(
89 action: &FileAction,
90 target_dir: &Path,
91 dry_run: bool,
92 force: bool,
93 rendered_content: Option<&str>,
94) -> Result<DeployResult> {
95 let target_path = target_dir.join(&action.target_rel_path);
96
97 if dry_run {
98 return Ok(DeployResult::DryRun);
99 }
100
101 if target_path.exists() || target_path.is_symlink() {
103 if target_path.is_symlink() {
104 std::fs::remove_file(&target_path)
105 .with_context(|| format!("failed to remove existing symlink: {}", target_path.display()))?;
106 } else if force {
107 std::fs::remove_file(&target_path)
108 .with_context(|| format!("failed to remove existing file: {}", target_path.display()))?;
109 } else {
110 return Ok(DeployResult::Conflict(format!(
111 "file already exists and is not managed by dotm: {}",
112 target_path.display()
113 )));
114 }
115 }
116
117 if let Some(parent) = target_path.parent() {
119 std::fs::create_dir_all(parent)
120 .with_context(|| format!("failed to create directory: {}", parent.display()))?;
121 }
122
123 match action.kind {
124 EntryKind::Template => {
125 let content = rendered_content.unwrap_or("");
126 std::fs::write(&target_path, content)
127 .with_context(|| format!("failed to write template output: {}", target_path.display()))?;
128 }
129 EntryKind::Base | EntryKind::Override => {
130 std::fs::copy(&action.source, &target_path)
131 .with_context(|| format!("failed to copy {} to {}", action.source.display(), target_path.display()))?;
132 copy_permissions(&action.source, &target_path)?;
133 }
134 }
135
136 Ok(DeployResult::Created)
137}
138
139pub fn apply_permission_override(path: &Path, mode_str: &str) -> Result<()> {
141 let mode = u32::from_str_radix(mode_str, 8)
142 .with_context(|| format!("invalid octal permission string: '{mode_str}'"))?;
143 let permissions = std::fs::Permissions::from_mode(mode);
144 std::fs::set_permissions(path, permissions)
145 .with_context(|| format!("failed to set permissions {mode_str} on {}", path.display()))?;
146 Ok(())
147}
148
149fn copy_permissions(source: &Path, dest: &Path) -> Result<()> {
151 let metadata = std::fs::metadata(source)
152 .with_context(|| format!("failed to read metadata from {}", source.display()))?;
153 std::fs::set_permissions(dest, metadata.permissions())
154 .with_context(|| format!("failed to set permissions on {}", dest.display()))?;
155 Ok(())
156}