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}