Skip to main content

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