rusty_store/storage.rs
1use ron::ser::PrettyConfig;
2use serde::{Deserialize, Serialize};
3use std::fmt::Debug;
4use std::fs::{self, File, OpenOptions};
5use std::io::{Read, Seek, Write};
6use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9use log::debug;
10use log::info;
11use log::warn;
12
13use crate::manager::StoreManager;
14
15#[derive(Error, Debug)]
16pub enum StoreError {
17 #[error("RON parsing error: {0}")]
18 RonParse(#[source] ron::error::SpannedError),
19
20 #[error("RON error: {0}")]
21 Ron(#[source] ron::error::Error),
22
23 #[error("Failed to open file: {0}")]
24 FileOpen(#[source] std::io::Error),
25
26 #[error("Failed to read from file: {0}")]
27 Read(#[source] std::io::Error),
28
29 #[error("Failed to create directory: {0}")]
30 CreateDir(#[source] std::io::Error),
31
32 #[error("Failed to write to file: {0}")]
33 Write(#[source] std::io::Error),
34}
35
36/// Represents the different types of storage locations that can be used for storing data.
37///
38/// The `StoringType` enum provides a way to specify where data should be stored, based on the
39/// platform and the type of data. It includes variants for common storage locations such as
40/// cache, data, and configuration directories, as well as a custom path option.
41///
42/// # Variants
43///
44/// - `Cache`: Path to the user's cache directory.
45/// - `Data`: Path to the user's data directory.
46/// - `Config`: Path to the user's configuration directory.
47/// - `Custom(PathBuf)`: Custom path specified by the user.
48#[derive(Debug, Default)]
49pub enum StoringType {
50 /// Path to the user's cache directory.
51 ///
52 /// |Platform | Value | Example |
53 /// | ------- | ----------------------------------- | ---------------------------- |
54 /// | Linux | `$XDG_CACHE_HOME` or `$HOME`/.cache | /home/alice/.cache |
55 /// | macOS | `$HOME`/Library/Caches | /Users/Alice/Library/Caches |
56 /// | Windows | `{FOLDERID_LocalAppData}` | C:\Users\Alice\AppData\Local |
57 Cache,
58
59 /// Path to the user's data directory.
60 ///
61 /// |Platform | Value | Example |
62 /// | ------- | ---------------------------------------- | ---------------------------------------- |
63 /// | Linux | `$XDG_DATA_HOME` or `$HOME`/.local/share | /home/alice/.local/share |
64 /// | macOS | `$HOME`/Library/Application Support | /Users/Alice/Library/Application Support |
65 /// | Windows | `{FOLDERID_RoamingAppData}` | C:\Users\Alice\AppData\Roaming |
66 #[default]
67 Data,
68
69 /// Path to the user's config directory.
70 ///
71 /// |Platform | Value | Example |
72 /// | ------- | ------------------------------------- | ---------------------------------------- |
73 /// | Linux | `$XDG_CONFIG_HOME` or `$HOME`/.config | /home/alice/.config |
74 /// | macOS | `$HOME`/Library/Application Support | /Users/Alice/Library/Application Support |
75 /// | Windows | `{FOLDERID_RoamingAppData}` | C:\Users\Alice\AppData\Roaming |
76 Config,
77
78 /// Custom path of the store save location
79 Custom(PathBuf),
80}
81
82/// Trait allowing a struct to be managed by `Storage`.
83///
84/// The `Storing` trait provides a way to specify the type of storage location for a struct.
85/// It requires the struct to implement `Serialize`, `Deserialize`, and `Default` traits.
86///
87/// # Example
88///
89/// ```
90/// use rusty_store::{Storage, StoreHandle, Storing, StoringType};
91/// use serde::{Deserialize, Serialize};
92///
93///
94/// #[derive(Serialize, Deserialize, Default)]
95/// struct MyStore {
96/// pub count: u32,
97/// }
98///
99/// impl Storing for MyStore {
100/// fn store_type() -> StoringType {
101/// StoringType::Data
102/// }
103/// }
104/// ```
105pub trait Storing: Serialize + for<'de> Deserialize<'de> + Default {
106 fn store_type() -> StoringType {
107 StoringType::default()
108 }
109}
110
111/// `StoreHandle` acts as a container that holds store data in memory and provides methods to access
112/// and modify this data. The `StoreHandle` does not manage storage directly but facilitates the read and
113/// write operations by holding the data and its identifier.
114#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
115pub struct StoreHandle<T> {
116 store_id: String,
117 store: T,
118}
119
120impl<T: Storing> StoreHandle<T> {
121 /// Creates a new `StoreHandle` instance with the given `store_id`.
122 ///
123 /// # Arguments
124 ///
125 /// * `store_id` - A string slice that holds the identifier for the store.
126 ///
127 /// # Returns
128 ///
129 /// A new `StoreHandle` instance with the specified `store_id` and a default store.
130 pub fn new(store_id: &str) -> Self {
131 Self {
132 store: T::default(),
133 store_id: store_id.to_owned(),
134 }
135 }
136
137 /// Sets the store data.
138 ///
139 /// # Arguments
140 ///
141 /// * `store` - The store data to be set.
142 fn set_store(&mut self, store: T) {
143 debug!("Setting store with id: {}", self.store_id);
144 self.store = store;
145 }
146
147 /// Returns a mutable reference to the stored data.
148 ///
149 /// # Returns
150 ///
151 /// A mutable reference to the stored data.
152 pub fn get_store_mut(&mut self) -> &mut T {
153 &mut self.store
154 }
155
156 /// Returns a reference to the stored data.
157 ///
158 /// # Returns
159 ///
160 /// A reference to the stored data.
161 pub fn get_store(&self) -> &T {
162 &self.store
163 }
164
165 /// Returns a reference to the store identifier.
166 ///
167 /// # Returns
168 ///
169 /// A reference to the store identifier.
170 pub fn store_id(&self) -> &str {
171 &self.store_id
172 }
173}
174
175/// Handles file system paths for reading from and writing to data storage.
176///
177/// The `Storage` struct provides a way to manage file paths used for storing data in different locations.
178/// It simplifies the process of accessing and modifying data by providing methods for these operations.
179///
180/// # Example
181///
182/// ```
183/// use rusty_store::{Storage, StoreHandle, Storing};
184/// use serde::{Deserialize, Serialize};
185///
186/// #[derive(Serialize, Deserialize, Default, Storing)]
187/// pub struct MyStore {
188/// pub count: u32,
189/// }
190///
191/// impl MyStore {
192/// fn increment_count(&mut self) {
193/// self.count += 1;
194/// }
195/// }
196///
197///
198/// // Initialize the Storage with the defaults
199/// let storage = Storage::new("APP_ID");
200///
201/// // Create a handle for managing the store data.
202/// let mut handle = StoreHandle::<MyStore>::new("my_store_id");
203///
204/// // Read existing store from storage
205/// storage
206/// .read(&mut handle)
207/// .expect("Failed to read from storage");
208///
209/// // Modify the store data
210/// let counter = handle.get_store_mut();
211///
212/// counter.increment_count();
213/// counter.increment_count();
214/// counter.increment_count();
215///
216/// // Write changes to disk
217/// storage
218/// .write(&mut handle)
219/// .expect("Failed to write to storage");
220///
221/// let counter = handle.get_store();
222///
223/// println!("Count: {}", counter.count);
224/// ```
225#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
226pub struct Storage {
227 cache_dir: PathBuf,
228 data_dir: PathBuf,
229 config_dir: PathBuf,
230}
231
232impl Storage {
233 /// Get the cache directory
234 pub fn cache_dir(&self) -> &Path {
235 &self.cache_dir
236 }
237
238 /// Get the data directory
239 pub fn data_dir(&self) -> &Path {
240 &self.data_dir
241 }
242
243 /// Get the config directory
244 pub fn config_dir(&self) -> &Path {
245 &self.config_dir
246 }
247
248 /// Creates a new `Storage` instance by obtaining the paths for cache, data, and configuration directories.
249 ///
250 /// # Panics
251 ///
252 /// - Panics if the cache directory, data directory, or configuration directory path cannot be determined.
253 pub fn new(app_id: &str) -> Self {
254 Self {
255 data_dir: dirs::data_dir()
256 .expect("Failed to determine cache directory path")
257 .join(app_id),
258 config_dir: dirs::config_dir()
259 .expect("Failed to determine data directory path")
260 .join(app_id),
261 cache_dir: dirs::cache_dir()
262 .expect("Failed to determine configuration directory path")
263 .join(app_id),
264 }
265 }
266
267 /// Creates a new `Storage` instance with specific cache, data, and config paths.
268 ///
269 /// # Arguments
270 ///
271 /// * `cache_dir` - A `PathBuf` representing the cache directory path.
272 /// * `data_dir` - A `PathBuf` representing the data directory path.
273 /// * `config_dir` - A `PathBuf` representing the configuration directory path.
274 ///
275 /// # Returns
276 ///
277 /// A new `Storage` instance with the specified cache, data, and config paths.
278 ///
279 /// # Example
280 ///
281 /// ```
282 /// use std::path::PathBuf;
283 /// use rusty_store::Storage;
284 ///
285 /// let cache_dir = PathBuf::from("/path/to/cache");
286 /// let data_dir = PathBuf::from("/path/to/data");
287 /// let config_dir = PathBuf::from("/path/to/config");
288 ///
289 /// let storage = Storage::from_dirs(cache_dir, data_dir, config_dir);
290 /// ```
291 pub fn from_dirs(cache_dir: PathBuf, data_dir: PathBuf, config_dir: PathBuf) -> Self {
292 Self {
293 cache_dir,
294 data_dir,
295 config_dir,
296 }
297 }
298
299 /// Returns a new StoreManager of type `T` with the given `store_id`
300 pub fn new_manager<T: Storing>(&self, store_id: &str) -> Result<StoreManager<T>, StoreError> {
301 StoreManager::<T>::new(self, store_id)
302 }
303
304 /// Returns a new Handle of type `T` with the given `store_id`
305 pub fn new_handle<T: Storing>(&self, store_id: &str) -> StoreHandle<T> {
306 StoreHandle::<T>::new(store_id)
307 }
308
309 /// Reads the store from a file and updates the provided `StoreHandle`.
310 /// If the file does not exist, it creates a default store if a default is available.
311 ///
312 /// # Example
313 ///
314 /// ```
315 /// use rusty_store::{Storage, StoreHandle, Storing};
316 /// use serde::{Deserialize, Serialize};
317 ///
318 /// #[derive(Serialize, Deserialize, Default, Storing)]
319 /// pub struct MyStore {
320 /// pub count: u32,
321 /// }
322 ///
323 /// impl MyStore {
324 /// fn increment_count(&mut self) {
325 /// self.count += 1;
326 /// }
327 /// }
328 ///
329 /// let storage = Storage::new("APP_ID");
330 /// let mut handle: StoreHandle<MyStore> = StoreHandle::new("my_store_id");
331 ///
332 /// storage.read(&mut handle).expect("Failed to read store");
333 ///
334 /// ```
335 pub fn read<T: Storing>(&self, handle: &mut StoreHandle<T>) -> Result<(), StoreError> {
336 debug!("Reading store with id: {}", handle.store_id());
337 self.open_file::<T, _>(
338 |file, handle| {
339 let store = Self::read_string(file).map_err(StoreError::Read)?;
340 let store_data: T = ron::from_str(&store).map_err(StoreError::RonParse)?;
341
342 handle.set_store(store_data);
343
344 info!("Successfully read store with id: {}", handle.store_id());
345 Ok(())
346 },
347 handle,
348 )
349 }
350
351 /// Writes the current store `T` from the provided `StoreHandle` to a file.
352 /// If the file does not exist, it creates a default store if a default is available.
353 ///
354 /// # Example
355 ///
356 /// ```
357 /// use rusty_store::{Storage, StoreHandle, Storing};
358 /// use serde::{Deserialize, Serialize};
359 ///
360 /// #[derive(Serialize, Deserialize, Default, Storing)]
361 /// pub struct MyStore {
362 /// pub count: u32,
363 /// }
364 ///
365 /// impl MyStore {
366 /// fn increment_count(&mut self) {
367 /// self.count += 1;
368 /// }
369 /// }
370 ///
371 /// let storage = Storage::new("APP_ID");
372 /// let mut handle: StoreHandle<MyStore> = StoreHandle::new("my_store_id");
373 ///
374 /// storage.write(&mut handle).expect("Failed to read store");
375 ///
376 /// ```
377 pub fn write<T: Storing>(&self, handle: &mut StoreHandle<T>) -> Result<(), StoreError> {
378 debug!("Writing store with id: {}", handle.store_id());
379 self.open_file::<T, _>(
380 |file: &mut File, handle| {
381 let store = handle.get_store_mut();
382
383 // Serialize current store to string
384 let str =
385 ron::ser::to_string_pretty(&store, PrettyConfig::new().compact_arrays(true))
386 .map_err(StoreError::Ron)?;
387
388 // Read current file content for comparison
389 file.rewind().map_err(StoreError::Write)?;
390 let existing = Self::read_string(file).map_err(StoreError::Read)?;
391
392 // Skip writing if identical
393 if existing == str {
394 debug!(
395 "Store unchanged, skipping write for id: {}",
396 handle.store_id()
397 );
398 return Ok(());
399 }
400
401 // Otherwise, overwrite file
402 file.set_len(0).map_err(StoreError::Write)?;
403 file.rewind().map_err(StoreError::Write)?;
404 file.write_all(str.as_bytes()).map_err(StoreError::Write)?;
405 file.flush().map_err(StoreError::Write)?;
406
407 info!("Successfully wrote store with id: {}", handle.store_id());
408 Ok(())
409 },
410 handle,
411 )
412 }
413
414 /// Opens the file for reading or writing. If the file does not exist, it attempts
415 /// to create a default store if a default is provided.
416 fn open_file<T, F>(
417 &self,
418 mut operation: F,
419 handle: &mut StoreHandle<T>,
420 ) -> Result<(), StoreError>
421 where
422 T: Storing,
423 F: FnMut(&mut File, &mut StoreHandle<T>) -> Result<(), StoreError>,
424 {
425 let mut dir_path = self.dir_path::<T>();
426 dir_path.push(handle.store_id()); // i don't like this
427
428 debug!("Opening file at path: {:?}", dir_path);
429
430 match OpenOptions::new()
431 .read(true)
432 .write(true)
433 .create(false)
434 .truncate(false)
435 .open(&dir_path)
436 {
437 Ok(mut config) => {
438 debug!("File opened successfully at path: {:?}", dir_path);
439 operation(&mut config, handle)
440 }
441 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
442 warn!(
443 "File not found at path: {:?}, creating default store",
444 dir_path
445 );
446 self.store_default::<T>(dir_path)?;
447 self.open_file(operation, handle)
448 }
449 Err(err) => {
450 warn!(
451 "Failed to open file at path: {:?}, error: {:?}",
452 dir_path, err
453 );
454 Err(StoreError::FileOpen(err))
455 }
456 }
457 }
458
459 fn store_default<T: Storing>(&self, path: PathBuf) -> Result<(), StoreError> {
460 debug!("Storing default configuration at path: {:?}", path);
461 if let Some(parent) = path.parent() {
462 fs::create_dir_all(parent).map_err(StoreError::CreateDir)?;
463 info!("Created directory for path: {:?}", parent);
464 }
465
466 let default_store = T::default();
467 let str = ron::ser::to_string_pretty(&default_store, PrettyConfig::new())
468 .map_err(StoreError::Ron)?;
469 fs::write(&path, str).map_err(StoreError::Write)?;
470 info!("Default store written at path: {:?}", &path);
471
472 Ok(())
473 }
474
475 fn dir_path<T: Storing>(&self) -> PathBuf {
476 let path = match T::store_type() {
477 StoringType::Cache => self.cache_dir.clone(),
478 StoringType::Data => self.data_dir.clone(),
479 StoringType::Config => self.config_dir.clone(),
480 StoringType::Custom(path) => path,
481 };
482 debug!(
483 "Resolved directory path for store type: {:?} to path: {:?}",
484 T::store_type(),
485 path
486 );
487 path
488 }
489
490 fn read_string(mut file: &File) -> Result<String, std::io::Error> {
491 let mut buf = String::new();
492 file.read_to_string(&mut buf)?;
493 debug!("Read string from file, length: {}", buf.len());
494 Ok(buf)
495 }
496}