Skip to main content

reinhardt_utils/storage/
memory.rs

1//! In-memory storage backend for testing and development
2
3use super::backend::Storage;
4use super::errors::{StorageError, StorageResult};
5use super::file::{FileMetadata, StoredFile};
6use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10
11/// File entry in memory storage
12#[derive(Clone, Debug)]
13struct MemoryFile {
14	content: Vec<u8>,
15	created_at: DateTime<Utc>,
16	modified_at: DateTime<Utc>,
17	accessed_at: DateTime<Utc>,
18}
19
20impl MemoryFile {
21	fn new(content: Vec<u8>) -> Self {
22		let now = Utc::now();
23		Self {
24			content,
25			created_at: now,
26			modified_at: now,
27			accessed_at: now,
28		}
29	}
30
31	fn update(&mut self, content: Vec<u8>) {
32		self.content = content;
33		self.modified_at = Utc::now();
34	}
35
36	fn access(&mut self) {
37		self.accessed_at = Utc::now();
38	}
39}
40
41/// In-memory storage backend
42#[derive(Clone)]
43pub struct InMemoryStorage {
44	files: Arc<RwLock<HashMap<String, MemoryFile>>>,
45	base_url: String,
46	location: String,
47	file_permissions_mode: Option<u32>,
48	directory_permissions_mode: Option<u32>,
49}
50
51impl InMemoryStorage {
52	/// Create a new in-memory storage backend
53	///
54	/// # Examples
55	///
56	/// ```
57	/// use reinhardt_utils::storage::InMemoryStorage;
58	///
59	/// let storage = InMemoryStorage::new("memory_root", "http://localhost/media");
60	/// assert_eq!(storage.base_location(), "memory_root");
61	/// assert_eq!(storage.base_url(), "http://localhost/media");
62	/// ```
63	pub fn new(location: impl Into<String>, base_url: impl Into<String>) -> Self {
64		Self {
65			files: Arc::new(RwLock::new(HashMap::new())),
66			base_url: base_url.into(),
67			location: location.into(),
68			file_permissions_mode: None,
69			directory_permissions_mode: None,
70		}
71	}
72	/// Set file and directory permission modes
73	///
74	/// # Examples
75	///
76	/// ```
77	/// use reinhardt_utils::storage::InMemoryStorage;
78	///
79	/// let storage = InMemoryStorage::new("memory_root", "http://localhost/media")
80	///     .with_permissions(Some(0o644), Some(0o755));
81	/// assert_eq!(storage.file_permissions_mode(), Some(0o644));
82	/// assert_eq!(storage.directory_permissions_mode(), Some(0o755));
83	/// ```
84	pub fn with_permissions(mut self, file_mode: Option<u32>, dir_mode: Option<u32>) -> Self {
85		self.file_permissions_mode = file_mode;
86		self.directory_permissions_mode = dir_mode;
87		self
88	}
89	/// Get the base location path
90	///
91	/// # Examples
92	///
93	/// ```
94	/// use reinhardt_utils::storage::InMemoryStorage;
95	///
96	/// let storage = InMemoryStorage::new("my_location", "http://localhost/media");
97	/// assert_eq!(storage.base_location(), "my_location");
98	/// ```
99	pub fn base_location(&self) -> &str {
100		&self.location
101	}
102	/// Get the base URL for file access
103	///
104	/// # Examples
105	///
106	/// ```
107	/// use reinhardt_utils::storage::InMemoryStorage;
108	///
109	/// let storage = InMemoryStorage::new("memory_root", "http://example.com/files");
110	/// assert_eq!(storage.base_url(), "http://example.com/files");
111	/// ```
112	pub fn base_url(&self) -> &str {
113		&self.base_url
114	}
115	/// Get the file permissions mode if set
116	///
117	/// # Examples
118	///
119	/// ```
120	/// use reinhardt_utils::storage::InMemoryStorage;
121	///
122	/// let storage = InMemoryStorage::new("memory_root", "http://localhost/media")
123	///     .with_permissions(Some(0o644), None);
124	/// assert_eq!(storage.file_permissions_mode(), Some(0o644));
125	///
126	/// let storage_no_perms = InMemoryStorage::new("memory_root", "http://localhost/media");
127	/// assert_eq!(storage_no_perms.file_permissions_mode(), None);
128	/// ```
129	pub fn file_permissions_mode(&self) -> Option<u32> {
130		self.file_permissions_mode
131	}
132	/// Get the directory permissions mode if set
133	///
134	/// # Examples
135	///
136	/// ```
137	/// use reinhardt_utils::storage::InMemoryStorage;
138	///
139	/// let storage = InMemoryStorage::new("memory_root", "http://localhost/media")
140	///     .with_permissions(None, Some(0o755));
141	/// assert_eq!(storage.directory_permissions_mode(), Some(0o755));
142	///
143	/// let storage_no_perms = InMemoryStorage::new("memory_root", "http://localhost/media");
144	/// assert_eq!(storage_no_perms.directory_permissions_mode(), None);
145	/// ```
146	pub fn directory_permissions_mode(&self) -> Option<u32> {
147		self.directory_permissions_mode
148	}
149	/// Deconstruct storage into components for serialization
150	///
151	/// Returns a tuple of (path, args, kwargs) compatible with Django storage conventions
152	///
153	/// # Examples
154	///
155	/// ```
156	/// use reinhardt_utils::storage::InMemoryStorage;
157	///
158	/// let storage = InMemoryStorage::new("memory_root", "http://localhost/media");
159	/// let (path, args, kwargs) = storage.deconstruct();
160	/// assert_eq!(path, "reinhardt_storage.InMemoryStorage");
161	/// assert_eq!(args, ());
162	/// assert_eq!(kwargs.get("location").unwrap(), "memory_root");
163	/// assert_eq!(kwargs.get("base_url").unwrap(), "http://localhost/media");
164	/// ```
165	pub fn deconstruct(&self) -> (&str, (), HashMap<String, String>) {
166		let mut kwargs = HashMap::new();
167		kwargs.insert("location".to_string(), self.location.clone());
168		kwargs.insert("base_url".to_string(), self.base_url.clone());
169		if let Some(mode) = self.file_permissions_mode {
170			kwargs.insert("file_permissions_mode".to_string(), format!("0o{:o}", mode));
171		}
172		if let Some(mode) = self.directory_permissions_mode {
173			kwargs.insert(
174				"directory_permissions_mode".to_string(),
175				format!("0o{:o}", mode),
176			);
177		}
178		("reinhardt_storage.InMemoryStorage", (), kwargs)
179	}
180}
181
182#[async_trait]
183impl Storage for InMemoryStorage {
184	async fn save(&self, path: &str, content: &[u8]) -> StorageResult<FileMetadata> {
185		let mut files = self.files.write().unwrap_or_else(|e| e.into_inner());
186
187		if let Some(existing) = files.get_mut(path) {
188			existing.update(content.to_vec());
189		} else {
190			let file = MemoryFile::new(content.to_vec());
191			files.insert(path.to_string(), file);
192		}
193
194		Ok(FileMetadata::new(path.to_string(), content.len() as u64))
195	}
196
197	async fn read(&self, path: &str) -> StorageResult<StoredFile> {
198		let mut files = self.files.write().unwrap_or_else(|e| e.into_inner());
199
200		let file = files
201			.get_mut(path)
202			.ok_or_else(|| StorageError::NotFound(path.to_string()))?;
203
204		file.access();
205
206		let metadata = FileMetadata::new(path.to_string(), file.content.len() as u64);
207		Ok(StoredFile::new(metadata, file.content.clone()))
208	}
209
210	async fn delete(&self, path: &str) -> StorageResult<()> {
211		let mut files = self.files.write().unwrap_or_else(|e| e.into_inner());
212
213		// Support directory deletion - remove all files with this prefix
214		if path.ends_with('/') || !files.contains_key(path) {
215			let prefix = path.trim_end_matches('/');
216			let to_remove: Vec<String> = files
217				.keys()
218				.filter(|k| k.starts_with(&format!("{}/", prefix)))
219				.cloned()
220				.collect();
221
222			for key in to_remove {
223				files.remove(&key);
224			}
225		} else {
226			files
227				.remove(path)
228				.ok_or_else(|| StorageError::NotFound(path.to_string()))?;
229		}
230
231		Ok(())
232	}
233
234	async fn exists(&self, path: &str) -> StorageResult<bool> {
235		let files = self.files.read().unwrap_or_else(|e| e.into_inner());
236
237		// Check exact match
238		if files.contains_key(path) {
239			return Ok(true);
240		}
241
242		// Check if it's a directory (has files with this prefix)
243		let prefix = format!("{}/", path.trim_end_matches('/'));
244		Ok(files.keys().any(|k| k.starts_with(&prefix)))
245	}
246
247	async fn metadata(&self, path: &str) -> StorageResult<FileMetadata> {
248		let files = self.files.read().unwrap_or_else(|e| e.into_inner());
249
250		let file = files
251			.get(path)
252			.ok_or_else(|| StorageError::NotFound(path.to_string()))?;
253
254		Ok(FileMetadata::new(
255			path.to_string(),
256			file.content.len() as u64,
257		))
258	}
259
260	async fn list(&self, path: &str) -> StorageResult<Vec<FileMetadata>> {
261		let files = self.files.read().unwrap_or_else(|e| e.into_inner());
262
263		let prefix = if path.is_empty() {
264			String::new()
265		} else {
266			format!("{}/", path.trim_end_matches('/'))
267		};
268
269		let mut results = Vec::new();
270		for (key, file) in files.iter() {
271			if path.is_empty() {
272				// Root level - only files without /
273				if !key.contains('/') {
274					results.push(FileMetadata::new(key.clone(), file.content.len() as u64));
275				}
276			} else if key.starts_with(&prefix) {
277				let relative = &key[prefix.len()..];
278				// Only direct children (no further /)
279				if !relative.contains('/') {
280					results.push(FileMetadata::new(key.clone(), file.content.len() as u64));
281				}
282			}
283		}
284
285		Ok(results)
286	}
287
288	fn url(&self, path: &str) -> String {
289		if path.is_empty() || path == "." {
290			return format!("{}/", self.base_url.trim_end_matches('/'));
291		}
292		format!(
293			"{}/{}",
294			self.base_url.trim_end_matches('/'),
295			path.trim_start_matches('/')
296		)
297	}
298
299	fn path(&self, name: &str) -> String {
300		name.to_string()
301	}
302
303	async fn get_accessed_time(&self, path: &str) -> StorageResult<DateTime<Utc>> {
304		let files = self.files.read().unwrap_or_else(|e| e.into_inner());
305
306		let file = files
307			.get(path)
308			.ok_or_else(|| StorageError::NotFound(path.to_string()))?;
309
310		Ok(file.accessed_at)
311	}
312
313	async fn get_created_time(&self, path: &str) -> StorageResult<DateTime<Utc>> {
314		let files = self.files.read().unwrap_or_else(|e| e.into_inner());
315
316		let file = files
317			.get(path)
318			.ok_or_else(|| StorageError::NotFound(path.to_string()))?;
319
320		Ok(file.created_at)
321	}
322
323	async fn get_modified_time(&self, path: &str) -> StorageResult<DateTime<Utc>> {
324		let files = self.files.read().unwrap_or_else(|e| e.into_inner());
325
326		let file = files
327			.get(path)
328			.ok_or_else(|| StorageError::NotFound(path.to_string()))?;
329
330		Ok(file.modified_at)
331	}
332}
333
334#[cfg(test)]
335mod tests {
336	use super::*;
337
338	#[tokio::test]
339	async fn test_inmemory_write_and_read() {
340		let storage = InMemoryStorage::new("memory_root", "http://localhost/media");
341
342		// Write string content
343		storage.save("file.txt", b"hello").await.unwrap();
344		let file = storage.read("file.txt").await.unwrap();
345		assert_eq!(file.content, b"hello");
346
347		// Write binary content
348		storage.save("file.dat", b"hello").await.unwrap();
349		let file = storage.read("file.dat").await.unwrap();
350		assert_eq!(file.content, b"hello");
351	}
352
353	#[tokio::test]
354	async fn test_inmemory_str_bytes_conversion() {
355		let storage = InMemoryStorage::new("memory_root", "http://localhost/media");
356
357		// InMemoryStorage handles conversion from str to bytes and back
358		storage.save("file.txt", b"hello").await.unwrap();
359		let file = storage.read("file.txt").await.unwrap();
360		assert_eq!(file.content, b"hello");
361
362		storage.save("file.dat", b"hello").await.unwrap();
363		let file = storage.read("file.dat").await.unwrap();
364		assert_eq!(file.content, b"hello");
365	}
366
367	#[tokio::test]
368	async fn test_inmemory_url_generation() {
369		let storage = InMemoryStorage::new("memory_root", "http://localhost/media");
370		assert_eq!(storage.url("test.txt"), "http://localhost/media/test.txt");
371
372		// Test with base_url ending with slash
373		let storage2 = InMemoryStorage::new("memory_root", "http://localhost/media/");
374		assert_eq!(storage2.url("test.txt"), "http://localhost/media/test.txt");
375	}
376
377	#[tokio::test]
378	async fn test_inmemory_url_with_none_filename() {
379		let storage = InMemoryStorage::new("memory_root", "/test_media_url/");
380		assert_eq!(storage.url(""), "/test_media_url/");
381	}
382
383	#[tokio::test]
384	async fn test_inmemory_deconstruction() {
385		let storage = InMemoryStorage::new("memory_root", "http://localhost/media");
386		let (path, args, kwargs) = storage.deconstruct();
387
388		assert_eq!(path, "reinhardt_storage.InMemoryStorage");
389		assert_eq!(args, ());
390		assert_eq!(kwargs.get("location").unwrap(), "memory_root");
391		assert_eq!(kwargs.get("base_url").unwrap(), "http://localhost/media");
392
393		// Test with permissions
394		let storage_with_perms = InMemoryStorage::new("custom_path", "http://example.com/")
395			.with_permissions(Some(0o755), Some(0o600));
396		let (_, _, kwargs) = storage_with_perms.deconstruct();
397
398		assert_eq!(kwargs.get("location").unwrap(), "custom_path");
399		assert_eq!(kwargs.get("base_url").unwrap(), "http://example.com/");
400		assert_eq!(kwargs.get("file_permissions_mode").unwrap(), "0o755");
401		assert_eq!(kwargs.get("directory_permissions_mode").unwrap(), "0o600");
402	}
403
404	#[tokio::test]
405	async fn test_inmemory_settings_changed() {
406		// Properties using settings values as defaults should be updated
407		let storage = InMemoryStorage::new("explicit_location", "explicit_base_url/")
408			.with_permissions(Some(0o666), Some(0o666));
409
410		assert_eq!(storage.base_location(), "explicit_location");
411		assert_eq!(storage.base_url(), "explicit_base_url/");
412		assert_eq!(storage.file_permissions_mode(), Some(0o666));
413		assert_eq!(storage.directory_permissions_mode(), Some(0o666));
414
415		// Test defaults
416		let defaults_storage = InMemoryStorage::new("media_root", "media_url/");
417		assert_eq!(defaults_storage.base_location(), "media_root");
418		assert_eq!(defaults_storage.base_url(), "media_url/");
419	}
420}