gglib_core/paths/ensure.rs
1//! Directory creation and verification utilities.
2//!
3//! Provides strategies for creating directories and verifying they are writable.
4//! The `DirectoryCreationStrategy` enum does NOT include interactive/prompt variants;
5//! adapter code should handle user interaction separately.
6
7use std::fs::{self, OpenOptions};
8use std::io::Write;
9use std::path::Path;
10
11use super::error::PathError;
12
13/// Strategy for how to handle missing directories when ensuring they exist.
14///
15/// Note: This enum is intentionally non-interactive. CLI or GUI code that needs
16/// to prompt users should handle that logic separately and then pass `AutoCreate`
17/// or `Disallow` to `ensure_directory`.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum DirectoryCreationStrategy {
20 /// Create directories automatically if they are missing.
21 #[default]
22 AutoCreate,
23 /// Do not create directories; return an error if missing.
24 Disallow,
25}
26
27/// Ensure the provided directory exists and is writable according to the chosen strategy.
28///
29/// If the directory exists, verifies it's actually a directory and is writable.
30/// If the directory doesn't exist, behavior depends on `strategy`:
31/// - `AutoCreate`: Creates the directory (and parents)
32/// - `Disallow`: Returns an error
33pub fn ensure_directory(path: &Path, strategy: DirectoryCreationStrategy) -> Result<(), PathError> {
34 if path.exists() {
35 if !path.is_dir() {
36 return Err(PathError::NotADirectory(path.to_path_buf()));
37 }
38 } else {
39 match strategy {
40 DirectoryCreationStrategy::AutoCreate => {
41 fs::create_dir_all(path).map_err(|e| PathError::CreateFailed {
42 path: path.to_path_buf(),
43 reason: e.to_string(),
44 })?;
45 }
46 DirectoryCreationStrategy::Disallow => {
47 return Err(PathError::DirectoryNotFound(path.to_path_buf()));
48 }
49 }
50 }
51
52 verify_writable(path)?;
53 Ok(())
54}
55
56/// Verify a directory is writable by attempting to create a test file.
57pub fn verify_writable(path: &Path) -> Result<(), PathError> {
58 let test_file = path.join(".gglib_write_test");
59 let result = OpenOptions::new()
60 .create(true)
61 .write(true)
62 .truncate(true)
63 .open(&test_file);
64
65 match result {
66 Ok(mut file) => {
67 file.write_all(b"test")
68 .map_err(|e| PathError::NotWritable {
69 path: path.to_path_buf(),
70 reason: e.to_string(),
71 })?;
72 drop(file);
73 let _ = fs::remove_file(&test_file);
74 Ok(())
75 }
76 Err(err) => Err(PathError::NotWritable {
77 path: path.to_path_buf(),
78 reason: err.to_string(),
79 }),
80 }
81}