vpr_core/repositories/
shared.rs

1//! Shared repository utilities.
2//!
3//! This module contains shared functions and types for managing patient data repositories
4//! and file system operations used across different repository types.
5//!
6//! ## Key Components
7//!
8//! - **Directory Operations**: Utilities for creating unique patient directories
9//!   (`create_uuid_and_shard_dir`) and recursive copying (`copy_dir_recursive`)
10//! - **Git Integration**: Functions for adding files to Git index (`add_directory_to_index`)
11
12use crate::error::{PatientError, PatientResult};
13use crate::ShardableUuid;
14use std::{
15    fs,
16    io::{self, ErrorKind},
17    path::{Path, PathBuf},
18};
19
20/// Creates a unique sharded directory within the base records directory.
21///
22/// This is the simple production API that generates UUIDs internally.
23/// Creates a unique sharded directory with a custom UUID source.
24///
25/// This version accepts a UUID generator for testing collision handling.
26/// Production code should use `create_uuid_and_shard_dir()` instead.
27///
28/// # Arguments
29///
30/// * `base_dir` - The base records directory.
31/// * `uuid_source` - A mutable closure that generates new `ShardableUuid` instances.
32///
33/// # Returns
34///
35/// Returns a tuple of the allocated `ShardableUuid` and the `PathBuf` to the created directory.
36///
37/// # Errors
38///
39/// Returns a `PatientError::PatientDirCreation` if:
40/// - directory creation fails after 5 attempts,
41/// - parent directory creation fails.
42pub(crate) fn create_uuid_and_shard_dir_with_source(
43    base_dir: &Path,
44    mut uuid_source: impl FnMut() -> ShardableUuid,
45) -> PatientResult<(ShardableUuid, PathBuf)> {
46    // Allocate a new UUID, but guard against pathological UUID collisions (or pre-existing
47    // directories from external interference) by limiting retries.
48    for _attempt in 0..5 {
49        let uuid = uuid_source();
50        let candidate = uuid.sharded_dir(base_dir);
51
52        if candidate.exists() {
53            continue;
54        }
55
56        if let Some(parent) = candidate.parent() {
57            fs::create_dir_all(parent).map_err(PatientError::PatientDirCreation)?;
58        }
59
60        match fs::create_dir(&candidate) {
61            Ok(()) => return Ok((uuid, candidate)),
62            Err(e) if e.kind() == ErrorKind::AlreadyExists => continue,
63            Err(e) => return Err(PatientError::PatientDirCreation(e)),
64        }
65    }
66
67    Err(PatientError::PatientDirCreation(io::Error::new(
68        ErrorKind::AlreadyExists,
69        "failed to allocate a unique patient directory after 5 attempts",
70    )))
71}
72
73/// Creates a unique sharded directory using an auto-generated UUID.
74///
75/// Simple wrapper for production use that generates UUIDs internally.
76/// For testing with deterministic UUIDs, use `create_uuid_and_shard_dir_with_source()`.
77///
78/// # Arguments
79///
80/// * `base_dir` - The base records directory.
81///
82/// # Returns
83///
84/// Returns a tuple of the allocated `ShardableUuid` and the `PathBuf` to the created directory.
85///
86/// # Errors
87///
88/// Returns a `PatientError::PatientDirCreation` if:
89/// - directory creation fails after 5 attempts,
90/// - parent directory creation fails.
91pub(crate) fn create_uuid_and_shard_dir(
92    base_dir: &Path,
93) -> PatientResult<(ShardableUuid, PathBuf)> {
94    create_uuid_and_shard_dir_with_source(base_dir, ShardableUuid::new)
95}
96
97/// Recursively copies a directory and its contents to a destination.
98///
99/// This function creates the destination directory if it doesn't exist and
100/// copies all files and subdirectories from the source to the destination.
101///
102/// # Arguments
103/// * `src` - Source directory path
104/// * `dst` - Destination directory path
105///
106/// # Errors
107/// Returns an `std::io::Error` if:
108/// - creating the destination directory fails,
109/// - reading source directory entries fails,
110/// - inspecting entry types fails,
111/// - copying a file fails.
112pub fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
113    if !dst.exists() {
114        fs::create_dir_all(dst)?;
115    }
116
117    for entry in fs::read_dir(src)? {
118        let entry = entry?;
119        let ty = entry.file_type()?;
120        let src_path = entry.path();
121        let dst_path = dst.join(entry.file_name());
122
123        if ty.is_dir() {
124            copy_dir_recursive(&src_path, &dst_path)?;
125        } else {
126            fs::copy(&src_path, &dst_path)?;
127        }
128    }
129
130    Ok(())
131}
132
133/// Adds all files in a directory to a Git index recursively.
134///
135/// This function traverses the directory tree and adds all files to the Git index,
136/// creating a tree that can be committed. It skips .git directories.
137///
138/// # Arguments
139/// * `index` - Mutable reference to the Git index
140/// * `dir` - Directory path to add to the index
141///
142/// # Errors
143/// Returns a `git2::Error` if:
144/// - traversing the directory tree fails,
145/// - inspecting file types fails,
146/// - adding a path to the Git index fails.
147pub fn add_directory_to_index(index: &mut git2::Index, dir: &Path) -> Result<(), git2::Error> {
148    fn add_recursive(
149        index: &mut git2::Index,
150        dir: &Path,
151        prefix: &Path,
152    ) -> Result<(), git2::Error> {
153        for entry in std::fs::read_dir(dir).map_err(|e| git2::Error::from_str(&e.to_string()))? {
154            let entry = entry.map_err(|e| git2::Error::from_str(&e.to_string()))?;
155            let path = entry.path();
156            let file_type = entry
157                .file_type()
158                .map_err(|e| git2::Error::from_str(&e.to_string()))?;
159
160            // Skip .git directories
161            if path.ends_with(".git") {
162                continue;
163            }
164
165            if file_type.is_file() {
166                let relative_path = path.strip_prefix(prefix).unwrap();
167                index.add_path(relative_path)?;
168            } else if file_type.is_dir() {
169                add_recursive(index, &path, prefix)?;
170            }
171        }
172        Ok(())
173    }
174
175    add_recursive(index, dir, dir)
176}