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}