bp3d_fs/dirs/
mod.rs

1// Copyright (c) 2021, BlockProject 3D
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without modification,
6// are permitted provided that the following conditions are met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//     * Redistributions in binary form must reproduce the above copyright notice,
11//       this list of conditions and the following disclaimer in the documentation
12//       and/or other materials provided with the distribution.
13//     * Neither the name of BlockProject 3D nor the names of its contributors
14//       may be used to endorse or promote products derived from this software
15//       without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29use crate::dirs::system::get_app_bundled_asset;
30use once_cell::sync::OnceCell;
31use std::path::{Path, PathBuf};
32
33pub mod system;
34
35/// Represents all possible errors when requesting app directories.
36pub enum Error {
37    /// The system is missing an application data directory.
38    MissingDataDir,
39
40    /// An io error has occurred while created some directory.
41    Io(std::io::Error),
42}
43
44impl From<std::io::Error> for Error {
45    fn from(err: std::io::Error) -> Self {
46        Self::Io(err)
47    }
48}
49
50/// Represents the application's directories.
51///
52/// Main entry point to obtain any directory for your application.
53///
54/// These APIs will fail as last resort. If they fail it usually means the system has a problem.
55/// The system may also include specific configuration to break applications on purpose,
56/// in which case these APIs will also fail.
57pub struct App<'a> {
58    name: &'a str,
59    data: OnceCell<PathBuf>,
60    cache: OnceCell<PathBuf>,
61    docs: OnceCell<PathBuf>,
62    logs: OnceCell<PathBuf>,
63    config: OnceCell<PathBuf>,
64}
65
66impl<'a> App<'a> {
67    /// Creates a new application.
68    ///
69    /// # Arguments
70    ///
71    /// * `name`: the name of the application.
72    ///
73    /// returns: App
74    pub fn new(name: &'a str) -> App<'a> {
75        App {
76            name,
77            data: OnceCell::new(),
78            cache: OnceCell::new(),
79            docs: OnceCell::new(),
80            logs: OnceCell::new(),
81            config: OnceCell::new(),
82        }
83    }
84
85    /// Returns the path to this application's files.
86    ///
87    /// Use this directory to store any information not intended to be user accessible.
88    ///
89    /// # Errors
90    ///
91    /// Returns a [MissingDataDir](self::Error::MissingDataDir) if this system doesn't have any application
92    /// writable location; this should never occur on any supported system except if such system is broken.
93    ///
94    /// Returns an [Io](self::Error::Io) if some directory couldn't be created.
95    pub fn get_data(&self) -> Result<&Path, Error> {
96        self.data
97            .get_or_try_init(|| {
98                let data = system::get_app_data()
99                    .ok_or(Error::MissingDataDir)?
100                    .join(self.name);
101                if !data.is_dir() {
102                    std::fs::create_dir_all(&data)?;
103                }
104                Ok(data)
105            })
106            .map(|v| v.as_ref())
107    }
108
109    /// Returns the path to this application's cache.
110    ///
111    /// Use this directory to store cached files such as downloads, intermediate files, etc.
112    ///
113    /// # Errors
114    ///
115    /// Returns an [Io](self::Error::Io) if some directory couldn't be created.
116    pub fn get_cache(&self) -> Result<&Path, Error> {
117        self.cache
118            .get_or_try_init(|| {
119                let cache = match system::get_app_cache() {
120                    None => self.get_data()?.join("Cache"),
121                    Some(cache) => cache.join(self.name),
122                };
123                if !cache.is_dir() {
124                    std::fs::create_dir(&cache)?;
125                }
126                Ok(cache)
127            })
128            .map(|v| v.as_ref())
129    }
130
131    /// Returns the path to this application's public documents.
132    ///
133    /// Use this directory to store any content the user should see and alter.
134    ///
135    /// # Errors
136    ///
137    /// Returns an [Io](self::Error::Io) if some directory couldn't be created.
138    pub fn get_documents(&self) -> Result<&Path, Error> {
139        // If this is OK then we must be running from a sandboxed system
140        // where the app has it's own public documents folder, otherwise
141        // create a "public" Documents directory inside the application data directory.
142        self.docs
143            .get_or_try_init(|| match system::get_app_documents() {
144                Some(docs) => Ok(docs),
145                None => {
146                    let docs = self.get_data()?.join("Documents");
147                    if !docs.is_dir() {
148                        std::fs::create_dir(&docs)?;
149                    }
150                    Ok(docs)
151                }
152            })
153            .map(|v| v.as_ref())
154    }
155
156    /// Returns the path to this application's logs.
157    ///
158    /// Use this directory to store all logs. The user can view and alter this directory.
159    ///
160    /// # Errors
161    ///
162    /// Returns an [Io](self::Error::Io) if some directory couldn't be created.
163    pub fn get_logs(&self) -> Result<&Path, Error> {
164        // Logs should be public and not contain any sensitive information, so store that in
165        // the app's public documents.
166        self.logs
167            .get_or_try_init(|| {
168                let logs = match system::get_app_logs() {
169                    None => self.get_documents()?.join("Logs"),
170                    Some(logs) => logs.join(self.name),
171                };
172                if !logs.is_dir() {
173                    std::fs::create_dir(&logs)?;
174                }
175                Ok(logs)
176            })
177            .map(|v| v.as_ref())
178    }
179
180    /// Returns the path to this application's config.
181    ///
182    /// Use this directory to store all configs for the current user.
183    /// This directory is not intended for direct user access.
184    ///
185    /// # Errors
186    ///
187    /// Returns an [Io](self::Error::Io) if some directory couldn't be created.
188    pub fn get_config(&self) -> Result<&Path, Error> {
189        self.config
190            .get_or_try_init(|| {
191                let config = match system::get_app_config() {
192                    None => self.get_data()?.join("Config"),
193                    Some(config) => config.join(self.name),
194                };
195                if !config.is_dir() {
196                    std::fs::create_dir(&config)?;
197                }
198                Ok(config)
199            })
200            .map(|v| v.as_ref())
201    }
202}
203
204impl<'a> Clone for App<'a> {
205    fn clone(&self) -> Self {
206        App {
207            name: self.name,
208            data: self.data.clone(),
209            cache: self.cache.clone(),
210            docs: self.docs.clone(),
211            logs: self.logs.clone(),
212            config: self.config.clone(),
213        }
214    }
215}
216
217/// Gets a path to an application asset.
218/// Returns None if the asset couldn't be found.
219///
220/// On apple platforms this will look into the app bundle, on other platforms this looks
221/// in a subdirectory named "Assets" next to the application executable.
222pub fn get_asset(file_name: &str) -> Option<PathBuf> {
223    get_app_bundled_asset(file_name)
224}
225
226#[cfg(test)]
227mod tests {
228    use crate::dirs::App;
229
230    fn assert_sync_send<T: Sync + Send>(x: T) -> T {
231        x
232    }
233
234    #[test]
235    fn test_sync_send() {
236        let obj = App::new("test");
237        let _ = assert_sync_send(obj);
238    }
239}