turbovault_core/
multi_vault.rs

1//! Multi-vault management system for enterprise deployments
2//!
3//! Enables managing multiple Obsidian vaults simultaneously with:
4//! - Vault isolation and independent lifecycle
5//! - Default vault concept
6//! - Setting inheritance and per-vault overrides
7//! - Centralized configuration
8
9use crate::prelude::*;
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13
14/// Information about a registered vault
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16pub struct VaultInfo {
17    /// Unique vault name
18    pub name: String,
19    /// Vault directory path
20    pub path: std::path::PathBuf,
21    /// Whether this is the active/default vault
22    pub is_default: bool,
23    /// Configuration for this vault
24    pub config: VaultConfig,
25}
26
27/// Multi-vault manager coordinating multiple vaults
28pub struct MultiVaultManager {
29    /// All registered vaults
30    vaults: Arc<RwLock<HashMap<String, VaultConfig>>>,
31    /// Currently active/default vault
32    default_vault: Arc<RwLock<String>>,
33    /// Server-level configuration
34    config: ServerConfig,
35}
36
37impl MultiVaultManager {
38    /// Create a new multi-vault manager from server configuration
39    pub fn new(config: ServerConfig) -> Result<Self> {
40        // Allow zero vaults - vaults can be added at runtime via add_vault tool
41        let vaults = Arc::new(RwLock::new(
42            config
43                .vaults
44                .iter()
45                .map(|v| (v.name.clone(), v.clone()))
46                .collect(),
47        ));
48
49        // Find default vault if any configured, otherwise None (will be set via set_active_vault)
50        let default_name = if config.vaults.is_empty() {
51            String::new() // Empty string indicates no default set
52        } else {
53            config
54                .vaults
55                .iter()
56                .find(|v| v.is_default)
57                .map(|v| v.name.clone())
58                .or_else(|| config.vaults.first().map(|v| v.name.clone()))
59                .unwrap_or_default()
60        };
61
62        Ok(Self {
63            vaults,
64            default_vault: Arc::new(RwLock::new(default_name)),
65            config,
66        })
67    }
68
69    /// Create an empty multi-vault manager (vault-agnostic server startup)
70    pub fn empty(config: ServerConfig) -> Result<Self> {
71        // Create with no vaults pre-configured
72        Ok(Self {
73            vaults: Arc::new(RwLock::new(HashMap::new())),
74            default_vault: Arc::new(RwLock::new(String::new())),
75            config,
76        })
77    }
78
79    /// Add a new vault to the manager
80    pub async fn add_vault(&self, vault_config: VaultConfig) -> Result<()> {
81        let mut vaults = self.vaults.write().await;
82
83        if vaults.contains_key(&vault_config.name) {
84            return Err(Error::invalid_path(format!(
85                "Vault '{}' already exists",
86                vault_config.name
87            )));
88        }
89
90        let is_first_vault = vaults.is_empty();
91        vaults.insert(vault_config.name.clone(), vault_config.clone());
92
93        // If this is the first vault, automatically set it as default
94        if is_first_vault {
95            drop(vaults); // Release write lock before acquiring default_vault lock
96            *self.default_vault.write().await = vault_config.name;
97        }
98
99        Ok(())
100    }
101
102    /// Remove a vault from the manager
103    pub async fn remove_vault(&self, name: &str) -> Result<()> {
104        let mut vaults = self.vaults.write().await;
105
106        if !vaults.contains_key(name) {
107            return Err(Error::not_found(format!("Vault '{}' not found", name)));
108        }
109
110        let current_default = self.default_vault.read().await;
111
112        // If removing the default vault, we need to handle it
113        if *current_default == name {
114            drop(current_default); // Release read lock
115            vaults.remove(name);
116
117            // If there are other vaults, set the first one as default; otherwise, clear it
118            if let Some((first_name, _)) = vaults.iter().next() {
119                *self.default_vault.write().await = first_name.clone();
120            } else {
121                *self.default_vault.write().await = String::new();
122            }
123        } else {
124            vaults.remove(name);
125        }
126
127        Ok(())
128    }
129
130    /// Get configuration for a specific vault
131    pub async fn get_vault_config(&self, name: &str) -> Result<VaultConfig> {
132        let vaults = self.vaults.read().await;
133        vaults
134            .get(name)
135            .cloned()
136            .ok_or_else(|| Error::not_found(format!("Vault '{}' not found", name)))
137    }
138
139    /// Get the active/default vault name
140    pub async fn get_active_vault(&self) -> String {
141        self.default_vault.read().await.clone()
142    }
143
144    /// Set a different vault as the active vault
145    pub async fn set_active_vault(&self, name: &str) -> Result<()> {
146        let vaults = self.vaults.read().await;
147
148        if !vaults.contains_key(name) {
149            return Err(Error::not_found(format!("Vault '{}' not found", name)));
150        }
151
152        *self.default_vault.write().await = name.to_string();
153        Ok(())
154    }
155
156    /// List all registered vaults
157    pub async fn list_vaults(&self) -> Result<Vec<VaultInfo>> {
158        let vaults = self.vaults.read().await;
159        let default = self.default_vault.read().await.clone();
160
161        let infos = vaults
162            .iter()
163            .map(|(name, config)| VaultInfo {
164                name: name.clone(),
165                path: config.path.clone(),
166                is_default: name == &default,
167                config: config.clone(),
168            })
169            .collect();
170
171        Ok(infos)
172    }
173
174    /// Get effective settings for a vault (inherited + overridden)
175    pub async fn get_effective_vault_settings(&self, vault_name: &str) -> Result<VaultConfig> {
176        let vault_config = self.get_vault_config(vault_name).await?;
177
178        // Start with server defaults
179        let effective = vault_config.clone();
180
181        // Apply vault-specific overrides
182        // (VaultConfig already contains the overrides, so we just return it)
183
184        Ok(effective)
185    }
186
187    /// Get vault count
188    pub async fn vault_count(&self) -> usize {
189        self.vaults.read().await.len()
190    }
191
192    /// Check if a vault exists
193    pub async fn vault_exists(&self, name: &str) -> bool {
194        self.vaults.read().await.contains_key(name)
195    }
196
197    /// Get the active vault config
198    pub async fn get_active_vault_config(&self) -> Result<VaultConfig> {
199        let active_name = self.default_vault.read().await.clone();
200        if active_name.is_empty() {
201            return Err(Error::not_found(
202                "No vault is currently active. Please add a vault using add_vault tool."
203                    .to_string(),
204            ));
205        }
206        self.get_vault_config(&active_name).await
207    }
208}
209
210impl Clone for MultiVaultManager {
211    fn clone(&self) -> Self {
212        Self {
213            vaults: self.vaults.clone(),
214            default_vault: self.default_vault.clone(),
215            config: self.config.clone(),
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    fn create_test_vault(name: &str, is_default: bool) -> VaultConfig {
225        VaultConfig {
226            name: name.to_string(),
227            path: std::path::PathBuf::from(format!("/tmp/{}", name)),
228            is_default,
229            watch_for_changes: None,
230            max_file_size: None,
231            allowed_extensions: None,
232            excluded_paths: None,
233            enable_caching: None,
234            cache_ttl: None,
235            template_dirs: None,
236            allowed_operations: None,
237        }
238    }
239
240    fn create_test_config() -> ServerConfig {
241        let mut config = ServerConfig::new();
242        config.vaults = vec![
243            create_test_vault("vault1", true),
244            create_test_vault("vault2", false),
245        ];
246        config
247    }
248
249    #[test]
250    fn test_multi_vault_manager_creation() {
251        let config = create_test_config();
252        let manager = MultiVaultManager::new(config);
253        assert!(manager.is_ok());
254    }
255
256    #[test]
257    fn test_can_create_empty_vaults() {
258        // Vault-agnostic design: allow empty vaults for runtime addition
259        let config = ServerConfig::new(); // Empty vaults
260        let manager = MultiVaultManager::new(config);
261        assert!(manager.is_ok());
262        let mgr = manager.unwrap();
263        // Should start with no default vault set
264        let rt = tokio::runtime::Runtime::new().unwrap();
265        let default = rt.block_on(async { mgr.get_active_vault().await });
266        assert!(default.is_empty());
267    }
268
269    #[tokio::test]
270    async fn test_get_active_vault() {
271        let config = create_test_config();
272        let manager = MultiVaultManager::new(config).unwrap();
273        let active = manager.get_active_vault().await;
274        assert_eq!(active, "vault1");
275    }
276
277    #[tokio::test]
278    async fn test_set_active_vault() {
279        let config = create_test_config();
280        let manager = MultiVaultManager::new(config).unwrap();
281
282        manager.set_active_vault("vault2").await.unwrap();
283        let active = manager.get_active_vault().await;
284        assert_eq!(active, "vault2");
285    }
286
287    #[tokio::test]
288    async fn test_set_invalid_active_vault() {
289        let config = create_test_config();
290        let manager = MultiVaultManager::new(config).unwrap();
291
292        let result = manager.set_active_vault("nonexistent").await;
293        assert!(result.is_err());
294    }
295
296    #[tokio::test]
297    async fn test_add_vault() {
298        let config = create_test_config();
299        let manager = MultiVaultManager::new(config).unwrap();
300
301        let new_vault = create_test_vault("vault3", false);
302        manager.add_vault(new_vault).await.unwrap();
303
304        assert!(manager.vault_exists("vault3").await);
305    }
306
307    #[tokio::test]
308    async fn test_add_duplicate_vault_fails() {
309        let config = create_test_config();
310        let manager = MultiVaultManager::new(config).unwrap();
311
312        let dup_vault = create_test_vault("vault1", false);
313        let result = manager.add_vault(dup_vault).await;
314        assert!(result.is_err());
315    }
316
317    #[tokio::test]
318    async fn test_remove_vault() {
319        let config = create_test_config();
320        let manager = MultiVaultManager::new(config).unwrap();
321
322        manager.remove_vault("vault2").await.unwrap();
323        assert!(!manager.vault_exists("vault2").await);
324    }
325
326    #[tokio::test]
327    async fn test_remove_default_vault_reassigns() {
328        // Removing the default vault should succeed and reassign to another vault
329        let config = create_test_config();
330        let manager = MultiVaultManager::new(config).unwrap();
331
332        // vault1 is the default, vault2 is the backup
333        assert_eq!(manager.get_active_vault().await, "vault1");
334
335        let result = manager.remove_vault("vault1").await;
336        assert!(result.is_ok());
337        assert!(!manager.vault_exists("vault1").await);
338
339        // Should now default to vault2
340        let new_default = manager.get_active_vault().await;
341        assert_eq!(new_default, "vault2");
342    }
343
344    #[tokio::test]
345    async fn test_list_vaults() {
346        let config = create_test_config();
347        let manager = MultiVaultManager::new(config).unwrap();
348
349        let vaults = manager.list_vaults().await.unwrap();
350        assert_eq!(vaults.len(), 2);
351        assert!(vaults.iter().any(|v| v.name == "vault1" && v.is_default));
352        assert!(vaults.iter().any(|v| v.name == "vault2" && !v.is_default));
353    }
354
355    #[tokio::test]
356    async fn test_vault_count() {
357        let config = create_test_config();
358        let manager = MultiVaultManager::new(config).unwrap();
359
360        assert_eq!(manager.vault_count().await, 2);
361
362        manager
363            .add_vault(create_test_vault("vault3", false))
364            .await
365            .ok();
366        assert_eq!(manager.vault_count().await, 3);
367    }
368
369    #[tokio::test]
370    async fn test_get_effective_vault_settings() {
371        let config = create_test_config();
372        let manager = MultiVaultManager::new(config).unwrap();
373
374        let settings = manager
375            .get_effective_vault_settings("vault1")
376            .await
377            .unwrap();
378        assert_eq!(settings.name, "vault1");
379    }
380
381    #[tokio::test]
382    async fn test_clone() {
383        let config = create_test_config();
384        let manager = MultiVaultManager::new(config).unwrap();
385        let manager2 = manager.clone();
386
387        assert_eq!(manager.vault_count().await, manager2.vault_count().await);
388    }
389}