pg_embedded_setup_unpriv/cache/operations/
copy.rs

1//! File and directory copy operations for the binary cache.
2//!
3//! Provides recursive directory copying with permission preservation.
4
5use camino::Utf8Path;
6use color_eyre::eyre::Context;
7use std::fs;
8use std::io;
9use std::path::Path;
10use tracing::debug;
11
12use crate::error::BootstrapResult;
13
14/// Observability target for cache operations.
15const LOG_TARGET: &str = "pg_embed::cache";
16
17/// Copies cached binaries to the target installation directory.
18///
19/// Performs a recursive copy of the source directory contents to the target,
20/// preserving directory structure and file permissions.
21///
22/// # Arguments
23///
24/// * `source` - Source directory containing cached binaries
25/// * `target` - Target installation directory
26///
27/// # Errors
28///
29/// Returns an error if:
30/// - The source directory does not exist or cannot be read
31/// - The target directory cannot be created
32/// - Any file copy operation fails
33///
34/// # Examples
35///
36/// ```no_run
37/// use camino::Utf8Path;
38/// use pg_embedded_setup_unpriv::cache::copy_from_cache;
39///
40/// let source = Utf8Path::new("/home/user/.cache/pg-embedded/binaries/17.4.0");
41/// let target = Utf8Path::new("/tmp/sandbox/install");
42/// copy_from_cache(source, target)?;
43/// # Ok::<(), color_eyre::Report>(())
44/// ```
45pub fn copy_from_cache(source: &Utf8Path, target: &Utf8Path) -> BootstrapResult<()> {
46    log_copy_start(source, target);
47
48    fs::create_dir_all(target)
49        .with_context(|| format!("failed to create target directory for cache copy: {target}"))?;
50
51    copy_dir_recursive(source.as_std_path(), target.as_std_path())
52        .with_context(|| format!("failed to copy cached binaries from {source} to {target}"))?;
53
54    log_copy_complete(source, target);
55    Ok(())
56}
57
58/// Logs the start of a cache copy operation.
59fn log_copy_start(source: &Utf8Path, target: &Utf8Path) {
60    debug!(
61        target: LOG_TARGET,
62        source = %source,
63        target = %target,
64        "copying binaries from cache"
65    );
66}
67
68/// Logs the completion of a cache copy operation.
69fn log_copy_complete(source: &Utf8Path, target: &Utf8Path) {
70    debug!(
71        target: LOG_TARGET,
72        source = %source,
73        target = %target,
74        "cache copy completed"
75    );
76}
77
78/// Recursively copies a directory and its contents.
79///
80/// Preserves directory structure and copies file metadata where possible.
81pub(crate) fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> {
82    if !dst.exists() {
83        fs::create_dir_all(dst)?;
84    }
85
86    for dir_entry in fs::read_dir(src)? {
87        let entry = dir_entry?;
88        let file_type = entry.file_type()?;
89        let src_path = entry.path();
90        let dst_path = dst.join(entry.file_name());
91
92        if file_type.is_dir() {
93            copy_dir_recursive(&src_path, &dst_path)?;
94        } else if file_type.is_symlink() {
95            copy_symlink(&src_path, &dst_path)?;
96        } else {
97            copy_file_with_permissions(&src_path, &dst_path)?;
98        }
99    }
100
101    copy_permissions(src, dst);
102
103    Ok(())
104}
105
106/// Copies a file preserving its permissions.
107fn copy_file_with_permissions(src: &Path, dst: &Path) -> io::Result<()> {
108    fs::copy(src, dst)?;
109    copy_permissions(src, dst);
110    Ok(())
111}
112
113/// Best-effort permission copy from source to destination.
114fn copy_permissions(src: &Path, dst: &Path) {
115    let Ok(metadata) = fs::metadata(src) else {
116        return;
117    };
118    if let Err(err) = fs::set_permissions(dst, metadata.permissions()) {
119        log_permission_copy_failure(src, dst, &err);
120    }
121}
122
123/// Logs a permission copy failure for debugging.
124fn log_permission_copy_failure(src: &Path, dst: &Path, err: &io::Error) {
125    debug!(
126        target: LOG_TARGET,
127        src = %src.display(),
128        dst = %dst.display(),
129        error = %err,
130        "failed to copy permissions (best effort)"
131    );
132}
133
134/// Copies a symbolic link.
135#[cfg(unix)]
136fn copy_symlink(src: &Path, dst: &Path) -> io::Result<()> {
137    let target = fs::read_link(src)?;
138    std::os::unix::fs::symlink(&target, dst)?;
139    Ok(())
140}
141
142#[cfg(not(unix))]
143fn copy_symlink(src: &Path, dst: &Path) -> io::Result<()> {
144    // On non-Unix, follow the symlink and copy the target.
145    // The symlink target is resolved by checking src directly (after following).
146    if src.is_file() {
147        fs::copy(src, dst)?;
148    } else if src.is_dir() {
149        copy_dir_recursive(src, dst)?;
150    } else {
151        return Err(io::Error::new(
152            io::ErrorKind::NotFound,
153            format!(
154                "symlink target does not exist or cannot be read: {}",
155                src.display()
156            ),
157        ));
158    }
159    Ok(())
160}