Skip to main content

subcog/services/
path_manager.rs

1//! Centralized path management for subcog storage locations.
2//!
3//! This module provides a unified interface for constructing and managing
4//! paths used by subcog's storage backends. It centralizes:
5//!
6//! - Path constants (directory names, file names)
7//! - Path construction methods for different storage types
8//! - Directory creation with proper error handling
9//!
10//! # Examples
11//!
12//! ```rust,ignore
13//! use subcog::services::PathManager;
14//! use std::path::Path;
15//!
16//! // For project-scoped storage (user-level data dir with project facets)
17//! let manager = PathManager::for_repo(Path::new("/path/to/repo"));
18//! let index_path = manager.index_path();
19//! let vector_path = manager.vector_path();
20//!
21//! // Ensure directories exist before creating backends
22//! manager.ensure_subcog_dir()?;
23//! ```
24
25use crate::storage::get_user_data_dir;
26use crate::{Error, Result};
27use std::path::{Path, PathBuf};
28
29/// Legacy name for the repo-local subcog directory (project storage no longer uses it).
30pub const SUBCOG_DIR_NAME: &str = ".subcog";
31
32/// Name of the `SQLite` index database file.
33pub const INDEX_DB_NAME: &str = "index.db";
34
35/// Name of the vector index file.
36pub const VECTOR_INDEX_NAME: &str = "vectors.idx";
37
38/// Name of the graph `SQLite` database file.
39pub const GRAPH_DB_NAME: &str = "graph.db";
40
41/// Manages storage paths for subcog backends.
42///
43/// `PathManager` provides a centralized way to construct paths for:
44/// - `SQLite` index databases
45/// - Vector similarity indices
46/// - The user-level data directory
47///
48/// Project scope uses the user-level data directory with project facets.
49#[derive(Debug, Clone)]
50pub struct PathManager {
51    /// Base directory for storage (user data dir).
52    base_dir: PathBuf,
53    /// The subcog data directory (same as base dir).
54    subcog_dir: PathBuf,
55}
56
57impl PathManager {
58    /// Creates a `PathManager` for repository-scoped storage.
59    ///
60    /// Storage paths will be within the user data directory.
61    /// Falls back to a temporary user-level directory if the user data dir cannot be resolved.
62    ///
63    /// # Arguments
64    ///
65    /// * `repo_root` - Path to the git repository root
66    ///
67    /// # Examples
68    ///
69    /// ```rust,ignore
70    /// let manager = PathManager::for_repo(Path::new("/home/user/project"));
71    /// // Uses user data directory, not repo-local storage
72    /// ```
73    #[must_use]
74    pub fn for_repo(_repo_root: impl AsRef<Path>) -> Self {
75        let base_dir = get_user_data_dir().unwrap_or_else(|err| {
76            tracing::warn!(
77                error = %err,
78                "Failed to resolve user data dir; falling back to temp dir"
79            );
80            std::env::temp_dir().join("subcog")
81        });
82        let subcog_dir = base_dir.clone();
83        Self {
84            base_dir,
85            subcog_dir,
86        }
87    }
88
89    /// Creates a `PathManager` for user-scoped storage.
90    ///
91    /// Storage paths will be directly within the user data directory
92    /// (no `.subcog` subdirectory).
93    ///
94    /// # Arguments
95    ///
96    /// * `user_data_dir` - Platform-specific user data directory
97    ///
98    /// # Examples
99    ///
100    /// ```rust,ignore
101    /// let manager = PathManager::for_user(Path::new("/home/user/.local/share/subcog"));
102    /// assert_eq!(manager.subcog_dir(), Path::new("/home/user/.local/share/subcog"));
103    /// ```
104    #[must_use]
105    pub fn for_user(user_data_dir: impl AsRef<Path>) -> Self {
106        let base_dir = user_data_dir.as_ref().to_path_buf();
107        // For user scope, the base dir IS the subcog dir (no .subcog subdirectory)
108        let subcog_dir = base_dir.clone();
109        Self {
110            base_dir,
111            subcog_dir,
112        }
113    }
114
115    /// Returns the base directory (user data dir).
116    #[must_use]
117    pub fn base_dir(&self) -> &Path {
118        &self.base_dir
119    }
120
121    /// Returns the subcog data directory.
122    ///
123    /// For project/user scope: `{user_data_dir}` (same as base)
124    #[must_use]
125    pub fn subcog_dir(&self) -> &Path {
126        &self.subcog_dir
127    }
128
129    /// Returns the path to the `SQLite` index database.
130    ///
131    /// # Returns
132    ///
133    /// `{subcog_dir}/index.db`
134    #[must_use]
135    pub fn index_path(&self) -> PathBuf {
136        self.subcog_dir.join(INDEX_DB_NAME)
137    }
138
139    /// Returns the path to the vector similarity index.
140    ///
141    /// # Returns
142    ///
143    /// `{subcog_dir}/vectors.idx`
144    #[must_use]
145    pub fn vector_path(&self) -> PathBuf {
146        self.subcog_dir.join(VECTOR_INDEX_NAME)
147    }
148
149    /// Returns the path to the graph `SQLite` database.
150    ///
151    /// # Returns
152    ///
153    /// `{subcog_dir}/graph.db`
154    #[must_use]
155    pub fn graph_path(&self) -> PathBuf {
156        self.subcog_dir.join(GRAPH_DB_NAME)
157    }
158
159    /// Ensures the subcog directory exists.
160    ///
161    /// Creates the directory and any necessary parent directories.
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if directory creation fails due to permissions
166    /// or other filesystem issues.
167    ///
168    /// # Examples
169    ///
170    /// ```rust,ignore
171    /// let manager = PathManager::for_repo(Path::new("/path/to/repo"));
172    /// manager.ensure_subcog_dir()?;
173    /// // Now safe to create backends at manager.index_path() and manager.vector_path()
174    /// ```
175    pub fn ensure_subcog_dir(&self) -> Result<()> {
176        std::fs::create_dir_all(&self.subcog_dir).map_err(|e| Error::OperationFailed {
177            operation: "create_subcog_dir".to_string(),
178            cause: format!(
179                "Cannot create {}: {}. Please create manually with: mkdir -p {}",
180                self.subcog_dir.display(),
181                e,
182                self.subcog_dir.display()
183            ),
184        })
185    }
186
187    /// Ensures the parent directory of a path exists.
188    ///
189    /// Useful for ensuring index or vector file parents exist before
190    /// creating backends.
191    ///
192    /// # Arguments
193    ///
194    /// * `path` - The path whose parent should be created
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if directory creation fails.
199    pub fn ensure_parent_dir(path: &Path) -> Result<()> {
200        if let Some(parent) = path.parent() {
201            std::fs::create_dir_all(parent).map_err(|e| Error::OperationFailed {
202                operation: "create_index_dir".to_string(),
203                cause: e.to_string(),
204            })?;
205        }
206        Ok(())
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use std::path::{Path, PathBuf};
214
215    #[test]
216    fn test_for_repo_paths() {
217        let manager = PathManager::for_repo("/home/user/project");
218        let expected_base =
219            get_user_data_dir().unwrap_or_else(|_| PathBuf::from("/home/user/project"));
220
221        assert_eq!(manager.base_dir(), expected_base.as_path());
222        assert_eq!(manager.subcog_dir(), expected_base.as_path());
223        assert_eq!(manager.index_path(), expected_base.join("index.db"));
224        assert_eq!(manager.vector_path(), expected_base.join("vectors.idx"));
225    }
226
227    #[test]
228    fn test_for_user_paths() {
229        let manager = PathManager::for_user("/home/user/.local/share/subcog");
230
231        assert_eq!(
232            manager.base_dir(),
233            Path::new("/home/user/.local/share/subcog")
234        );
235        // For user scope, subcog_dir equals base_dir
236        assert_eq!(
237            manager.subcog_dir(),
238            Path::new("/home/user/.local/share/subcog")
239        );
240        assert_eq!(
241            manager.index_path(),
242            Path::new("/home/user/.local/share/subcog/index.db")
243        );
244        assert_eq!(
245            manager.vector_path(),
246            Path::new("/home/user/.local/share/subcog/vectors.idx")
247        );
248    }
249
250    #[test]
251    fn test_constants() {
252        assert_eq!(SUBCOG_DIR_NAME, ".subcog");
253        assert_eq!(INDEX_DB_NAME, "index.db");
254        assert_eq!(VECTOR_INDEX_NAME, "vectors.idx");
255    }
256
257    #[test]
258    fn test_ensure_subcog_dir() {
259        let temp_dir = std::env::temp_dir().join("subcog_path_manager_test");
260        let _ = std::fs::remove_dir_all(&temp_dir); // Clean up from previous runs
261
262        let manager = PathManager::for_user(&temp_dir);
263
264        // Directory should not exist yet
265        assert!(!manager.subcog_dir().exists());
266
267        // Create it
268        manager
269            .ensure_subcog_dir()
270            .expect("Failed to create subcog dir");
271
272        // Now it should exist
273        assert!(manager.subcog_dir().exists());
274
275        // Cleanup
276        let _ = std::fs::remove_dir_all(&temp_dir);
277    }
278
279    #[test]
280    fn test_ensure_parent_dir() {
281        let temp_dir = std::env::temp_dir().join("subcog_path_manager_parent_test");
282        let _ = std::fs::remove_dir_all(&temp_dir);
283
284        let nested_path = temp_dir.join("deeply").join("nested").join("file.db");
285
286        // Parent should not exist
287        assert!(!nested_path.parent().unwrap().exists());
288
289        // Ensure parent exists
290        PathManager::ensure_parent_dir(&nested_path).expect("Failed to create parent");
291
292        // Now parent should exist
293        assert!(nested_path.parent().unwrap().exists());
294
295        // Cleanup
296        let _ = std::fs::remove_dir_all(&temp_dir);
297    }
298}