Skip to main content

horto_os_ui_shared/kits/
fs.rs

1//! Filesystem helpers that respect [`HostContext`] dry-run vs apply.
2
3use crate::context::HostContext;
4use crate::error::{HortoError, Result};
5use std::fs;
6use std::path::Path;
7use walkdir::WalkDir;
8
9/// Ensure `path` exists as a directory (`mkdir -p`).
10///
11/// In dry-run mode, records a planned action and returns without creating.
12///
13/// # Errors
14///
15/// Returns [`HortoError::Io`] on create failure in apply mode.
16pub fn ensure_dir(ctx: &mut HostContext, path: &Path) -> Result<()> {
17    if ctx.is_dry_run() {
18        ctx.plan_action(format!("mkdir -p {}", path.display()));
19        return Ok(());
20    }
21    fs::create_dir_all(path)?;
22    Ok(())
23}
24
25/// Write `content` to `path`, creating parent directories as needed.
26///
27/// In dry-run mode, records a planned write (byte length only) and returns.
28///
29/// # Errors
30///
31/// Returns [`HortoError::Io`] on parent create or write failure in apply mode.
32pub fn write_file(ctx: &mut HostContext, path: &Path, content: &[u8]) -> Result<()> {
33    if ctx.is_dry_run() {
34        ctx.plan_action(format!(
35            "write {} ({} bytes)",
36            path.display(),
37            content.len()
38        ));
39        return Ok(());
40    }
41    if let Some(parent) = path.parent() {
42        fs::create_dir_all(parent)?;
43    }
44    fs::write(path, content)?;
45    Ok(())
46}
47
48/// Copy a single file from `src` to `dest`, creating the dest parent as needed.
49///
50/// In dry-run mode, records a planned `cp` and returns.
51///
52/// # Errors
53///
54/// Returns [`HortoError::Message`] when the copy fails in apply mode (includes paths).
55pub fn copy_file(ctx: &mut HostContext, src: &Path, dest: &Path) -> Result<()> {
56    if ctx.is_dry_run() {
57        ctx.plan_action(format!("cp {} -> {}", src.display(), dest.display()));
58        return Ok(());
59    }
60    if let Some(parent) = dest.parent() {
61        fs::create_dir_all(parent)?;
62    }
63    fs::copy(src, dest).map_err(|e| {
64        HortoError::msg(format!("copy {} -> {}: {e}", src.display(), dest.display()))
65    })?;
66    Ok(())
67}
68
69/// Recursively copy contents of `src` into `dest` (like `cp -a src/. dest/`).
70///
71/// In dry-run mode, records one planned action and returns without walking.
72///
73/// # Errors
74///
75/// Returns [`HortoError::Io`] on directory/file create or copy failure in apply mode.
76pub fn copy_dir_contents(ctx: &mut HostContext, src: &Path, dest: &Path) -> Result<()> {
77    if ctx.is_dry_run() {
78        ctx.plan_action(format!("cp -a {}/. -> {}/", src.display(), dest.display()));
79        return Ok(());
80    }
81    fs::create_dir_all(dest)?;
82    for entry in WalkDir::new(src)
83        .into_iter()
84        .filter_map(std::result::Result::ok)
85    {
86        let path = entry.path();
87        let rel = path.strip_prefix(src).unwrap_or(path);
88        let target = dest.join(rel);
89        if entry.file_type().is_dir() {
90            fs::create_dir_all(&target)?;
91        } else if entry.file_type().is_file() {
92            if let Some(parent) = target.parent() {
93                fs::create_dir_all(parent)?;
94            }
95            fs::copy(path, &target)?;
96        }
97    }
98    Ok(())
99}
100
101/// Remove a file, symlink, or directory tree at `path`.
102///
103/// In dry-run mode, records a planned `rm -f` and returns.
104///
105/// # Errors
106///
107/// Returns [`HortoError::Io`] on remove failure in apply mode.
108pub fn remove_path(ctx: &mut HostContext, path: &Path) -> Result<()> {
109    if ctx.is_dry_run() {
110        ctx.plan_action(format!("rm -f {}", path.display()));
111        return Ok(());
112    }
113    if path.is_symlink() || path.is_file() {
114        fs::remove_file(path)?;
115    } else if path.is_dir() {
116        fs::remove_dir_all(path)?;
117    }
118    Ok(())
119}
120
121/// Set Unix mode bits on `path` (`chmod`).
122///
123/// In dry-run mode, records a planned chmod and returns. On non-Unix apply builds,
124/// the call is a no-op.
125///
126/// # Errors
127///
128/// Returns [`HortoError::Io`] when `set_permissions` fails on Unix apply mode.
129pub fn chmod(ctx: &mut HostContext, path: &Path, mode: u32) -> Result<()> {
130    if ctx.is_dry_run() {
131        ctx.plan_action(format!("chmod {mode:o} {}", path.display()));
132        return Ok(());
133    }
134    #[cfg(unix)]
135    {
136        use std::os::unix::fs::PermissionsExt;
137        let perms = fs::Permissions::from_mode(mode);
138        fs::set_permissions(path, perms)?;
139    }
140    #[cfg(not(unix))]
141    {
142        let _ = (path, mode);
143    }
144    Ok(())
145}