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}