Skip to main content

Mountain/Environment/
StorageProvider.rs

1//! # StorageProvider (Environment)
2//!
3//! Implements the `StorageProvider` trait for `MountainEnvironment`. Contains
4//! the core logic for Memento storage: reading from and writing to JSON
5//! storage files on disk.
6//!
7//! ## Storage scopes
8//!
9//! - **Global** (`IsGlobalScope = true`) - application-level key-value store
10//!   shared across all workspaces; persisted to `GlobalMementoPath`. Used for
11//!   user preferences, extension state.
12//! - **Workspace** (`IsGlobalScope = false`) - workspace-specific state;
13//!   persisted to `WorkspaceMementoPath` (reloaded on workspace change via
14//!   `UpdateWorkspaceMementoPathAndReload`). Used for workspace configs.
15//!
16//! ## Storage operations
17//!
18//! - `GetStorageValue(scope, key)` - reads from in-memory `HashMap`; returns
19//!   `None` for missing or empty keys; rejects keys > 1 024 chars.
20//! - `UpdateStorageValue(scope, key, value)` - inserts or removes key; rejects
21//!   values > 10 MB; spawns async `SaveStorageToDisk` after each mutation.
22//! - `GetAllStorage(scope)` - returns the full in-memory map as JSON.
23//! - `SetAllStorage(scope, state)` - overwrites the full map and persists.
24//!
25//! ## Async persistence
26//!
27//! All disk writes go through `SaveStorageToDisk`, which is spawned via
28//! `tokio::spawn` so the trait call returns immediately. The function creates
29//! parent directories as needed and logs errors without propagating them
30//! (fire-and-forget pattern). Writes are NOT yet atomic (temp+rename); that
31//! is a known TODO.
32//!
33//! ## VS Code reference
34//!
35//! - `vs/platform/storage/common/storageService.ts`
36//! - `vs/platform/storage/common/memento.ts`
37
38use std::{collections::HashMap, path::PathBuf};
39
40use CommonLibrary::{Error::CommonError::CommonError, Storage::StorageProvider::StorageProvider};
41use async_trait::async_trait;
42use serde_json::Value;
43use tokio::fs;
44
45use super::{MountainEnvironment::MountainEnvironment, Utility};
46use crate::dev_log;
47
48// TODO: storage quotas per extension, encryption for sensitive values,
49// compression for large datasets, migration/versioning, atomic writes
50// (temp+rename), storage change notifications/watchers, TTL / auto-expiry,
51// binary data support, transaction (batch + rollback), sync via Air.
52#[async_trait]
53impl StorageProvider for MountainEnvironment {
54	/// Retrieves a value from either global or workspace storage.
55	/// Includes defensive validation to prevent invalid keys and invalid JSON.
56	async fn GetStorageValue(&self, IsGlobalScope:bool, Key:&str) -> Result<Option<Value>, CommonError> {
57		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
58
59		dev_log!(
60			"storage",
61			"[StorageProvider] Getting value from {} scope for key: {}",
62			ScopeName,
63			Key
64		);
65
66		// Validate key to prevent injection or invalid storage paths
67		if Key.is_empty() {
68			return Ok(None);
69		}
70
71		if Key.len() > 1024 {
72			return Err(CommonError::InvalidArgument {
73				ArgumentName:"Key".into(),
74				Reason:"Key length exceeds maximum allowed length of 1024 characters".into(),
75			});
76		}
77
78		let StorageMapMutex = if IsGlobalScope {
79			&self.ApplicationState.Configuration.MementoGlobalStorage
80		} else {
81			&self.ApplicationState.Configuration.MementoWorkspaceStorage
82		};
83
84		let StorageMapGuard = StorageMapMutex
85			.lock()
86			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
87
88		Ok(StorageMapGuard.get(Key).cloned())
89	}
90
91	/// Updates or deletes a value in either global or workspace storage.
92	/// Includes comprehensive validation for key length, value size, and JSON
93	/// validity.
94	async fn UpdateStorageValue(
95		&self,
96
97		IsGlobalScope:bool,
98
99		Key:String,
100
101		ValueToSet:Option<Value>,
102	) -> Result<(), CommonError> {
103		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
104
105		// Per-key updates fire at every workbench state change (sidebar
106		// view state, panel layout, editor tab order, telemetry opt-ins).
107		// Short-form + long-form both emit under `storage-verbose` so the
108		// default log stays clean; `Trace=storage-verbose` restores
109		// the original verbose tracing.
110		if crate::IPC::DevLog::IsShort::Fn() {
111			crate::dev_log!("storage-verbose", "update {} {}", ScopeName, Key);
112		} else {
113			dev_log!(
114				"storage-verbose",
115				"[StorageProvider] Updating value in {} scope for key: {}",
116				ScopeName,
117				Key
118			);
119		}
120
121		// Validate key to prevent injection or invalid storage paths
122		if Key.is_empty() {
123			return Err(CommonError::InvalidArgument {
124				ArgumentName:"Key".into(),
125				Reason:"Key cannot be empty".into(),
126			});
127		}
128
129		if Key.len() > 1024 {
130			return Err(CommonError::InvalidArgument {
131				ArgumentName:"Key".into(),
132				Reason:"Key length exceeds maximum allowed length of 1024 characters".into(),
133			});
134		}
135
136		// If setting a value, validate it's not too large
137		if let Some(ref value) = ValueToSet {
138			if let Ok(json_string) = serde_json::to_string(value) {
139				if json_string.len() > 10 * 1024 * 1024 {
140					// 10MB limit per value
141					return Err(CommonError::InvalidArgument {
142						ArgumentName:"ValueToSet".into(),
143						Reason:"Value size exceeds maximum allowed size of 10MB".into(),
144					});
145				}
146			}
147		}
148
149		let (StorageMapMutex, StoragePathOption) = if IsGlobalScope {
150			(
151				self.ApplicationState.Configuration.MementoGlobalStorage.clone(),
152				Some(
153					self.ApplicationState
154						.GlobalMementoPath
155						.lock()
156						.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
157						.clone(),
158				),
159			)
160		} else {
161			(
162				self.ApplicationState.Configuration.MementoWorkspaceStorage.clone(),
163				self.ApplicationState
164					.WorkspaceMementoPath
165					.lock()
166					.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
167					.clone(),
168			)
169		};
170
171		// Perform the in-memory update.
172		let DataToSave = {
173			let mut StorageMapGuard = StorageMapMutex
174				.lock()
175				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
176
177			if let Some(Value) = ValueToSet {
178				StorageMapGuard.insert(Key, Value);
179			} else {
180				StorageMapGuard.remove(&Key);
181			}
182
183			StorageMapGuard.clone()
184		};
185
186		if let Some(StoragePath) = StoragePathOption {
187			tokio::spawn(async move {
188				SaveStorageToDisk(StoragePath, DataToSave).await;
189			});
190		}
191
192		Ok(())
193	}
194
195	/// Retrieves the entire storage map for a given scope.
196	async fn GetAllStorage(&self, IsGlobalScope:bool) -> Result<Value, CommonError> {
197		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
198
199		dev_log!(
200			"storage-verbose",
201			"[StorageProvider] Getting all values from {} scope.",
202			ScopeName
203		);
204
205		let StorageMapMutex = if IsGlobalScope {
206			&self.ApplicationState.Configuration.MementoGlobalStorage
207		} else {
208			&self.ApplicationState.Configuration.MementoWorkspaceStorage
209		};
210
211		let StorageMapGuard = StorageMapMutex
212			.lock()
213			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
214
215		Ok(serde_json::to_value(&*StorageMapGuard)?)
216	}
217
218	/// Overwrites the entire storage map for a given scope and persists it.
219	async fn SetAllStorage(&self, IsGlobalScope:bool, FullState:Value) -> Result<(), CommonError> {
220		let ScopeName = if IsGlobalScope { "Global" } else { "Workspace" };
221
222		dev_log!(
223			"storage-verbose",
224			"[StorageProvider] Setting all values for {} scope.",
225			ScopeName
226		);
227
228		let DeserializedState:HashMap<String, Value> = serde_json::from_value(FullState)?;
229
230		let (StorageMapMutex, StoragePathOption) = if IsGlobalScope {
231			(
232				self.ApplicationState.Configuration.MementoGlobalStorage.clone(),
233				Some(
234					self.ApplicationState
235						.GlobalMementoPath
236						.lock()
237						.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
238						.clone(),
239				),
240			)
241		} else {
242			(
243				self.ApplicationState.Configuration.MementoWorkspaceStorage.clone(),
244				self.ApplicationState
245					.WorkspaceMementoPath
246					.lock()
247					.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
248					.clone(),
249			)
250		};
251
252		// Update in-memory state
253		*StorageMapMutex
254			.lock()
255			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)? = DeserializedState.clone();
256
257		// Persist to disk asynchronously
258		if let Some(StoragePath) = StoragePathOption {
259			tokio::spawn(async move {
260				SaveStorageToDisk(StoragePath, DeserializedState).await;
261			});
262		}
263
264		Ok(())
265	}
266}
267
268// --- Internal Helper Functions ---
269
270/// An internal helper function to asynchronously write the storage map to a
271/// file.
272async fn SaveStorageToDisk(Path:PathBuf, Data:HashMap<String, Value>) {
273	// Fires on every `storage:updateItems` that mutates the global map
274	// (~50 per session during workbench boot alone). The failure path
275	// below logs unconditionally; the success path is per-call noise.
276	dev_log!(
277		"storage-verbose",
278		"[StorageProvider] Persisting storage to disk: {}",
279		Path.display()
280	);
281
282	match serde_json::to_string_pretty(&Data) {
283		Ok(JSONString) => {
284			if let Some(ParentDirectory) = Path.parent() {
285				if let Err(Error) = fs::create_dir_all(ParentDirectory).await {
286					dev_log!(
287						"storage",
288						"error: [StorageProvider] Failed to create parent directory for '{}': {}",
289						Path.display(),
290						Error
291					);
292
293					return;
294				}
295			}
296
297			if let Err(Error) = fs::write(&Path, JSONString).await {
298				dev_log!(
299					"storage",
300					"error: [StorageProvider] Failed to write storage file to '{}': {}",
301					Path.display(),
302					Error
303				);
304			}
305		},
306
307		Err(Error) => {
308			dev_log!(
309				"storage",
310				"error: [StorageProvider] Failed to serialize storage data for '{}': {}",
311				Path.display(),
312				Error
313			);
314		},
315	}
316}