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}