Skip to main content

dotm/
deployer.rs

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
15/// Deploy a file by creating a symlink from target to the source file.
16///
17/// Used for user-mode Base and Override files. The symlink points to the
18/// canonicalized absolute path of the source file in packages/.
19pub 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    // Directory at target is an error
34    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    // Existing symlink (broken or pointing elsewhere): remove it
42    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        // Regular file: conflict unless force
47        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    // Create parent directories
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    // Create symlink to canonicalized source path
65    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
77/// Deploy a file by copying content directly to the target.
78///
79/// Used for templates (rendered content) and system-mode files.
80/// Templates get rendered content written; base/override files are copied from source.
81pub 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    // Directory at target is an error
97    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    // Create parent directories
120    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
145/// Parse an octal mode string (e.g. "755") and apply it to the file at `path`.
146pub 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
155/// Copy the Unix file permissions from `source` to `dest`.
156fn 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}