Skip to main content

bevy_settings/
store_fs.rs

1use bevy_log::{debug, error, warn};
2use bevy_platform::dirs::preferences_dir;
3use bevy_tasks::IoTaskPool;
4use std::{fs, path::PathBuf};
5
6/// Persistent storage which uses the local filesystem. Settings will be located in the
7/// OS-specific directory for user settings.
8pub(crate) struct SettingsStore {
9    base_path: Option<PathBuf>,
10}
11
12impl SettingsStore {
13    /// Construct a new filesystem settings store.
14    ///
15    /// # Arguments
16    /// * `app_name` - The name of the application. See [`crate::SettingsPlugin`] for usage.
17    pub(crate) fn new(app_name: &str) -> Self {
18        Self {
19            base_path: if let Some(base_dir) = preferences_dir() {
20                let prefs_path = base_dir.join(app_name);
21                debug!("Settings path: {:?}", prefs_path);
22                Some(prefs_path)
23            } else {
24                warn!("Could not find user configuration directories");
25                None
26            },
27        }
28    }
29
30    /// Save a [`toml::Table`] to disk.
31    ///
32    /// # Arguments
33    /// * `filename` - the name of the file to be saved
34    /// * `contents` - the contents of the file
35    pub(crate) fn save(&self, filename: &str, contents: toml::Table) {
36        if let Some(base_path) = &self.base_path {
37            // Recursively create the settings directory if it doesn't exist.
38            let mut dir_builder = fs::DirBuilder::new();
39            dir_builder.recursive(true);
40            if let Err(e) = dir_builder.create(base_path.clone()) {
41                warn!("Could not create settings directory: {:?}", e);
42                return;
43            }
44
45            // Save settings to temp file
46            let temp_path = base_path.join(format!("{filename}.toml.new"));
47            if let Err(e) = fs::write(&temp_path, contents.to_string()) {
48                error!("Error saving settings file: {}", e);
49            }
50
51            // Replace old settings file with new one.
52            let file_path = base_path.join(format!("{filename}.toml"));
53            if let Err(e) = fs::rename(&temp_path, file_path) {
54                warn!("Could not save settings file: {:?}", e);
55            }
56        }
57    }
58
59    /// Save the contents of a [`toml::Table`] to disk in another thread.
60    ///
61    /// # Arguments
62    /// * `filename` - the name of the file to be saved
63    /// * `contents` - the contents of the file
64    pub(crate) fn save_async(&self, filename: &str, contents: toml::Table) {
65        if let Some(base_path) = &self.base_path {
66            IoTaskPool::get().scope(|scope| {
67                scope.spawn(async {
68                    // Recursively create the settings directory if it doesn't exist.
69                    let mut dir_builder = fs::DirBuilder::new();
70                    dir_builder.recursive(true);
71                    if let Err(e) = dir_builder.create(base_path.clone()) {
72                        warn!("Could not create settings directory: {:?}", e);
73                        return;
74                    }
75
76                    // Save settings to temp file
77                    let temp_path = base_path.join(format!("{filename}.toml.new"));
78                    if let Err(e) = fs::write(&temp_path, contents.to_string()) {
79                        error!("Error saving settings file: {}", e);
80                    }
81
82                    // Replace old settings file with new one.
83                    let file_path = base_path.join(format!("{filename}.toml"));
84                    if let Err(e) = fs::rename(&temp_path, file_path) {
85                        warn!("Could not save settings file: {:?}", e);
86                    }
87                });
88            });
89        }
90    }
91
92    /// Deserialize a [`toml::Table`] from disk. If the file does not exist, `None` will
93    /// be returned.
94    ///
95    /// # Arguments
96    /// * `filename` - The name of the settings file, without the file extension.
97    pub(crate) fn load(&self, filename: &str) -> Option<toml::Table> {
98        let Some(base_path) = &self.base_path else {
99            return None;
100        };
101
102        let file_path = base_path.join(format!("{filename}.toml"));
103        decode_toml_file(&file_path)
104    }
105}
106
107/// Load a settings file from disk in TOML format.
108pub(crate) fn decode_toml_file(file: &PathBuf) -> Option<toml::Table> {
109    if file.exists() && file.is_file() {
110        let settings_str = match fs::read_to_string(file) {
111            Ok(settings_str) => settings_str,
112            Err(e) => {
113                error!("Error reading settings file: {}", e);
114                return None;
115            }
116        };
117
118        let table_value = match toml::from_str::<toml::Value>(&settings_str) {
119            Ok(table_value) => table_value,
120            Err(e) => {
121                error!("Error parsing settings file: {}", e);
122                return None;
123            }
124        };
125
126        match table_value {
127            toml::Value::Table(table) => Some(table),
128            _ => {
129                error!("Settings file must be a table");
130                None
131            }
132        }
133    } else {
134        // Settings file does not exist yet.
135        None
136    }
137}