Skip to main content

Mountain/Environment/
StorageProvider.rs

1//! # StorageProvider (Environment)
2//!
3//! Implements the `StorageProvider` trait for `MountainEnvironment`, providing
4//! persistent key-value storage for extensions and application components.
5//!
6//! ## RESPONSIBILITIES
7//!
8//! ### 1. Storage Management
9//! - Provide global storage (shared across all workspaces)
10//! - Provide workspace-scoped storage (per workspace)
11//! - Support namespaced keys to avoid collisions
12//! - Handle storage quota limits
13//!
14//! ### 2. CRUD Operations
15//! - `Get(scope, key)`: Retrieve value by key
16//! - `Set(scope, key, value)`: Store value (create or update)
17//! - `Remove(scope, key)`: Delete key-value pair
18//! - `Clear(scope)`: Remove all keys in a scope
19//! - `Keys(scope)`: List all keys in a scope
20//!
21//! ### 3. Data Persistence
22//! - Write storage changes to disk immediately
23//! - Load storage from disk on startup
24//! - Handle corrupted storage files with recovery
25//! - Backup storage before writes (optional)
26//!
27//! ### 4. Type Safety
28//! - Store and retrieve arbitrary JSON-serializable values
29//! - Handle serialization/deserialization errors
30//! - Support primitive types and complex objects
31//!
32//! ## ARCHITECTURAL ROLE
33//!
34//! StorageProvider is the **persistent key-value store** for Mountain:
35//!
36//! ```text
37//! Extension ──► StorageProvider ──► Disk (JSON files)
38//!                      │
39//!                      └─► ApplicationState (Cache)
40//! ```
41//!
42//! ### Position in Mountain
43//! - `Environment` module: Persistence capability provider
44//! - Implements `CommonLibrary::Storage::StorageProvider` trait
45//! - Accessible via `Environment.Require<dyn StorageProvider>()`
46//!
47//! ### Storage Scopes
48//! - **Global**: `{AppData}/User/globalStorage.json`
49//!   - Shared across all workspaces
50//!   - Used for user preferences, extension state
51//! - **Workspace**:
52//!   `{AppData}/User/workspaceStorage/{workspace-id}/storage.json`
53//!   - Specific to current workspace
54//!   - Used for workspace-specific settings and state
55//!
56//! ### Storage Format
57//! - JSON file with simple key-value pairs
58//! - Values are `serde_json::Value` (any JSON type)
59//! - Keys are strings with namespace prefix (e.g., "extensionId.setting")
60//!
61//! ### Dependencies
62//! - `ApplicationState`: Access to global/workspace memento maps
63//! - `FileSystemWriter`: To persist storage to disk
64//! - `Log`: Storage change logging
65//!
66//! ### Dependents
67//! - Extensions: Store extension-specific state
68//! - `ConfigurationProvider`: Uses global storage for user settings
69//! - `ExtensionManagement`: Store extension metadata
70//! - Any component needing persistent settings
71//!
72//! ## STORAGE LIFECYCLE
73//!
74//! 1. **App Start**: `ApplicationState::default()` loads global and workspace
75//!    memento
76//! 2. **Workspace Change**: `UpdateWorkspaceMementoPathAndReload()` loads new
77//!    workspace storage
78//! 3. **Runtime**: Providers read/write to in-memory maps (`GlobalMemento`,
79//!    `WorkspaceMemento`)
80//! 4. **Shutdown**: `ApplicationRunTime::SaveApplicationState()` writes memento
81//!    to disk
82//! 5. **Crash Recovery**: `Internal::LoadInitialMementoFromDisk()` with
83//!    backup/restore
84//!
85//! ## ERROR HANDLING
86//!
87//! - Disk full: `CommonError::FileSystemIO`
88//! - Permission denied: `CommonError::FileSystemIO`
89//! - JSON parse error: `CommonError::SerializationError` (with recovery)
90//! - Quota exceeded: `CommonError::StorageFull` (TODO)
91//!
92//! ## PERFORMANCE
93//!
94//! - All storage operations are in-memory (fast)
95//! - Disk writes are async and batched
96//! - Consider size limits (configurable max per storage file)
97//! - Large values (>1MB) should be stored in files, not storage
98//!
99//! ## RECOVERY MECHANISMS
100//!
101//! - Corrupted JSON files are backed up with timestamps
102//! - On parse error, storage is reset to empty and continues
103//! - Directories are created automatically
104//! - Writes are atomic (write to temp, then rename)
105//!
106//! ## VS CODE REFERENCE
107//!
108//! Patterns from VS Code:
109//! - `vs/platform/storage/common/storageService.ts` - Storage service
110//! - `vs/platform/storage/common/memento.ts` - Memento pattern for state
111//!
112//! ## TODO
113//!
114//! - [ ] Implement storage quotas (per-extension limits)
115//! - [ ] Add storage encryption for sensitive data
116//! - [ ] Support storage compression for large datasets
117//! - [ ] Implement storage migration/versioning
118//! - [ ] Add storage inspection and debugging tools
119//! - [ ] Support storage syncing across devices (via Air)
120//! - [ ] Implement storage TTL (time-to-live) for auto-expiring keys
121//! - [ ] Add storage subscriptions/notifications on change
122//! - [ ] Support binary data storage (not just JSON)
123//! - [ ] Implement storage transactions (batch operations with rollback)
124//!
125//! ## MODULE CONTENTS
126//!
127//! - [`StorageProvider`]: Main struct implementing the trait
128//! - Storage access methods (Get, Set, Remove, Clear, Keys)
129//! - Memento loading and saving
130//! - Recovery and backup logic
131
132// Responsibilities:
133//   - Core logic for Memento storage operations.
134//   - Reading from and writing to global and workspace JSON storage files.
135//   - Provides both per-key and high-performance batch operations.
136//   - Enhances keychain integration with the `keyring` crate for secure storage.
137//   - Adds secure storage with encryption for sensitive data.
138//   - Handles storage errors gracefully with proper error handling.
139//   - Manages storage file location and directory creation.
140//   - Supports both global (application-level) and workspace-specific storage.
141//
142// TODOs:
143//   - Implement encryption for sensitive data in JSON storage
144//   - Add storage migration support for version upgrades
145//   - Implement storage compression for large datasets
146//   - Add storage change notifications and watchers
147//   - Implement storage quota management
148//   - Add storage backup and restore functionality
149//   - Support storage sync across multiple devices
150//   - Implement storage conflict resolution
151//   - Add storage validation and schema checking
152//   - Support storage transaction support for atomic operations
153//   - Implement storage garbage collection for deprecated keys
154//   - Add storage performance metrics and optimization
155//   - Support storage for secrets via the SecretProvider (keychain)
156//   - Implement cache invalidation on external changes
157//
158// Inspired by VSCode's secrets service which:
159// - Uses operating system keychain for secure secret storage
160// - Provides consistent API across platforms
161// - Handles keychain access failures gracefully
162// - Implements secret encryption and secure storage
163// - Supports secret sharing between processes
164//
165// ## Storage Scopes
166//
167// 1. **Global Storage**: Application-level settings that persist across all workspaces
168//    - Location: App config directory (platform-specific)
169//    - File: `global.json` or similar
170//    - Scope: `IsGlobalScope = true`
171//    - Use case: User preferences, extension settings
172//
173// 2. **Workspace Storage**: Workspace-specific settings and state
174//    - Location: Within workspace directory (usually `.vscode/storage`)
175//    - File: `workspace.json`
176//    - Scope: `IsGlobalScope = false`
177//    - Use case: Workspace-specific configurations, workspace state
178//
179// ## Storage Operations
180//
181// - **GetStorageValue**: Retrieve a single key value
182// - **UpdateStorageValue**: Set or delete a single key value
183// - **GetAllStorage**: Retrieve the entire storage map
184// - **SetAllStorage**: Replace the entire storage map
185//
186// Persistence is handled asynchronously via `tokio::spawn` to avoid blocking
187// the main thread while writing to disk.
188//
189// ## Error Handling
190//
191// The provider handles various error conditions:
192// - File I/O errors (read/write/creation)
193// - Serialization/deserialization errors
194// - Directory creation failures
195// - Lock poisoning from concurrent access
196// - Missing permissions for storage paths
197
198//! # StorageProvider Implementation
199//!
200//! Implements the `StorageProvider` trait for the `MountainEnvironment`. This
201//! provider contains the core logic for Memento storage operations, including
202//! reading from and writing to the appropriate JSON storage files on disk.
203
204use std::{collections::HashMap, path::PathBuf};
205
206use CommonLibrary::{Error::CommonError::CommonError, Storage::StorageProvider::StorageProvider};
207use async_trait::async_trait;
208use serde_json::Value;
209use tokio::fs;
210
211use super::{MountainEnvironment::MountainEnvironment, Utility};
212use crate::dev_log;
213
214#[async_trait]
215impl StorageProvider for MountainEnvironment {
216	/// Retrieves a value from either global or workspace storage.
217	/// Includes defensive validation to prevent invalid keys and invalid JSON.
218	async fn GetStorageValue(&self, IsGlobalScope:bool, Key:&str) -> Result<Option<Value>, CommonError> {
219		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
220
221		dev_log!(
222			"storage",
223			"[StorageProvider] Getting value from {} scope for key: {}",
224			ScopeName,
225			Key
226		);
227
228		// Validate key to prevent injection or invalid storage paths
229		if Key.is_empty() {
230			return Ok(None);
231		}
232
233		if Key.len() > 1024 {
234			return Err(CommonError::InvalidArgument {
235				ArgumentName:"Key".into(),
236				Reason:"Key length exceeds maximum allowed length of 1024 characters".into(),
237			});
238		}
239
240		let StorageMapMutex = if IsGlobalScope {
241			&self.ApplicationState.Configuration.MementoGlobalStorage
242		} else {
243			&self.ApplicationState.Configuration.MementoWorkspaceStorage
244		};
245
246		let StorageMapGuard = StorageMapMutex
247			.lock()
248			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
249
250		Ok(StorageMapGuard.get(Key).cloned())
251	}
252
253	/// Updates or deletes a value in either global or workspace storage.
254	/// Includes comprehensive validation for key length, value size, and JSON
255	/// validity.
256	async fn UpdateStorageValue(
257		&self,
258
259		IsGlobalScope:bool,
260
261		Key:String,
262
263		ValueToSet:Option<Value>,
264	) -> Result<(), CommonError> {
265		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
266
267		// Per-key updates fire at every workbench state change (sidebar
268		// view state, panel layout, editor tab order, telemetry opt-ins).
269		// Short-form + long-form both emit under `storage-verbose` so the
270		// default log stays clean; `Trace=storage-verbose` restores
271		// the original verbose tracing.
272		if crate::IPC::DevLog::IsShort() {
273			crate::dev_log!("storage-verbose", "update {} {}", ScopeName, Key);
274		} else {
275			dev_log!(
276				"storage-verbose",
277				"[StorageProvider] Updating value in {} scope for key: {}",
278				ScopeName,
279				Key
280			);
281		}
282
283		// Validate key to prevent injection or invalid storage paths
284		if Key.is_empty() {
285			return Err(CommonError::InvalidArgument {
286				ArgumentName:"Key".into(),
287				Reason:"Key cannot be empty".into(),
288			});
289		}
290
291		if Key.len() > 1024 {
292			return Err(CommonError::InvalidArgument {
293				ArgumentName:"Key".into(),
294				Reason:"Key length exceeds maximum allowed length of 1024 characters".into(),
295			});
296		}
297
298		// If setting a value, validate it's not too large
299		if let Some(ref value) = ValueToSet {
300			if let Ok(json_string) = serde_json::to_string(value) {
301				if json_string.len() > 10 * 1024 * 1024 {
302					// 10MB limit per value
303					return Err(CommonError::InvalidArgument {
304						ArgumentName:"ValueToSet".into(),
305						Reason:"Value size exceeds maximum allowed size of 10MB".into(),
306					});
307				}
308			}
309		}
310
311		let (StorageMapMutex, StoragePathOption) = if IsGlobalScope {
312			(
313				self.ApplicationState.Configuration.MementoGlobalStorage.clone(),
314				Some(
315					self.ApplicationState
316						.GlobalMementoPath
317						.lock()
318						.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
319						.clone(),
320				),
321			)
322		} else {
323			(
324				self.ApplicationState.Configuration.MementoWorkspaceStorage.clone(),
325				self.ApplicationState
326					.WorkspaceMementoPath
327					.lock()
328					.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
329					.clone(),
330			)
331		};
332
333		// Perform the in-memory update.
334		let DataToSave = {
335			let mut StorageMapGuard = StorageMapMutex
336				.lock()
337				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
338
339			if let Some(Value) = ValueToSet {
340				StorageMapGuard.insert(Key, Value);
341			} else {
342				StorageMapGuard.remove(&Key);
343			}
344
345			StorageMapGuard.clone()
346		};
347
348		if let Some(StoragePath) = StoragePathOption {
349			tokio::spawn(async move {
350				SaveStorageToDisk(StoragePath, DataToSave).await;
351			});
352		}
353
354		Ok(())
355	}
356
357	/// Retrieves the entire storage map for a given scope.
358	async fn GetAllStorage(&self, IsGlobalScope:bool) -> Result<Value, CommonError> {
359		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
360
361		dev_log!(
362			"storage-verbose",
363			"[StorageProvider] Getting all values from {} scope.",
364			ScopeName
365		);
366
367		let StorageMapMutex = if IsGlobalScope {
368			&self.ApplicationState.Configuration.MementoGlobalStorage
369		} else {
370			&self.ApplicationState.Configuration.MementoWorkspaceStorage
371		};
372
373		let StorageMapGuard = StorageMapMutex
374			.lock()
375			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
376
377		Ok(serde_json::to_value(&*StorageMapGuard)?)
378	}
379
380	/// Overwrites the entire storage map for a given scope and persists it.
381	async fn SetAllStorage(&self, IsGlobalScope:bool, FullState:Value) -> Result<(), CommonError> {
382		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
383
384		dev_log!(
385			"storage-verbose",
386			"[StorageProvider] Setting all values for {} scope.",
387			ScopeName
388		);
389
390		let DeserializedState:HashMap<String, Value> = serde_json::from_value(FullState)?;
391
392		let (StorageMapMutex, StoragePathOption) = if IsGlobalScope {
393			(
394				self.ApplicationState.Configuration.MementoGlobalStorage.clone(),
395				Some(
396					self.ApplicationState
397						.GlobalMementoPath
398						.lock()
399						.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
400						.clone(),
401				),
402			)
403		} else {
404			(
405				self.ApplicationState.Configuration.MementoWorkspaceStorage.clone(),
406				self.ApplicationState
407					.WorkspaceMementoPath
408					.lock()
409					.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
410					.clone(),
411			)
412		};
413
414		// Update in-memory state
415		*StorageMapMutex
416			.lock()
417			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)? = DeserializedState.clone();
418
419		// Persist to disk asynchronously
420		if let Some(StoragePath) = StoragePathOption {
421			tokio::spawn(async move {
422				SaveStorageToDisk(StoragePath, DeserializedState).await;
423			});
424		}
425
426		Ok(())
427	}
428}
429
430// --- Internal Helper Functions ---
431
432/// An internal helper function to asynchronously write the storage map to a
433/// file.
434async fn SaveStorageToDisk(Path:PathBuf, Data:HashMap<String, Value>) {
435	// Fires on every `storage:updateItems` that mutates the global map
436	// (~50 per session during workbench boot alone). The failure path
437	// below logs unconditionally; the success path is per-call noise.
438	dev_log!(
439		"storage-verbose",
440		"[StorageProvider] Persisting storage to disk: {}",
441		Path.display()
442	);
443
444	match serde_json::to_string_pretty(&Data) {
445		Ok(JSONString) => {
446			if let Some(ParentDirectory) = Path.parent() {
447				if let Err(Error) = fs::create_dir_all(ParentDirectory).await {
448					dev_log!(
449						"storage",
450						"error: [StorageProvider] Failed to create parent directory for '{}': {}",
451						Path.display(),
452						Error
453					);
454
455					return;
456				}
457			}
458
459			if let Err(Error) = fs::write(&Path, JSONString).await {
460				dev_log!(
461					"storage",
462					"error: [StorageProvider] Failed to write storage file to '{}': {}",
463					Path.display(),
464					Error
465				);
466			}
467		},
468
469		Err(Error) => {
470			dev_log!(
471				"storage",
472				"error: [StorageProvider] Failed to serialize storage data for '{}': {}",
473				Path.display(),
474				Error
475			);
476		},
477	}
478}