spacetimedb_paths/lib.rs
1//! The spacetimedb directory structure, represented as a type hierarchy.
2//!
3//! # Directory Structure of the Database.
4//!
5//! [`SpacetimePaths`] holds the paths to the various directories used by the CLI & database.
6//!
7//! * **cli-bin-dir**: a directory under which all versions of all
8//! SpacetimeDB binaries is be stored. Each binary is stored in a
9//! directory named with version number of the binary in this directory. If a
10//! binary has any related files required by that binary which are specific to
11//! that version, for example, template configuration files, these files will be
12//! installed in this folder as well.
13//!
14//! * **cli-config-dir**: a directory where configuration and state for the CLI,
15//! as well as the keyfiles used by the server, are stored.
16//!
17//! * **cli-bin-file**: the location of the default spacetime CLI executable, which
18//! is a symlink to the actual `spacetime` binary in the cli-bin-dir.
19//!
20//! * **data-dir**: the directory where all persistent server & database files
21//! are stored.
22//!
23//! ## Unix Directory Structure
24//!
25//! On Unix-like platforms, such as Linux and macOS, the installation paths follow the
26//! XDG conventions by default:
27//!
28//! * `cli-config-dir`: `$XDG_CONFIG_HOME/spacetime/`
29//! * `cli-bin-dir`: `$XDG_DATA_HOME/spacetime/bin/`
30//! * `cli-bin-file`: `$XDG_BIN_HOME/spacetime`
31//! * `data-dir`: `$XDG_DATA_HOME/spacetime/data`
32//!
33//! As per the XDG base directory specification, those base directories fall back to
34//! to the following defaults if the corresponding environment variable is not set:
35//!
36//! * `$XDG_CONFIG_HOME`: `$HOME/.config`
37//! * `$XDG_DATA_HOME`: `$HOME/.local/share`
38//! * `$XDG_BIN_HOME`: `$HOME/.local/bin`
39//!
40//! For reference, the below is an example installation using the default paths:
41//!
42//!```sh
43//! $HOME
44//! ├── .local
45//! │ ├── bin
46//! │ │ └── spacetime -> $HOME/.local/share/spacetime/bin/1.10.1/spacetimedb-update # Current, in $PATH
47//! │ └── share
48//! │ └── spacetime
49//! │ ├── bin
50//! │ | └── 1.10.1
51//! │ | ├── spacetimedb-update # Version manager
52//! │ | ├── spacetimedb-cli # CLI
53//! │ | ├── spacetimedb-standalone # Server
54//! │ | ├── spacetimedb-cloud # Server
55//! │ | ├── cli.default.toml # Template CLI configuration file
56//! │ | └── config.default.toml # Template server configuration file
57//! | └── data/
58//! |
59//! └── .config
60//! └── spacetime
61//! ├── id_ecdsa # Private key
62//! ├── id_ecdsa.pub # Public key
63//! └── cli.toml # CLI configuration
64//! ```
65//!
66//!## Windows Directory Structure
67//!
68//! On Windows the installation paths follow Windows conventions, and is equivalent
69//! to a Root Directory (as defined below) at `%LocalAppData%\SpacetimeDB\`.
70//!
71//! > **Note**: the `SpacetimeDB` directory is in `%LocalAppData%` and not `%AppData%`.
72//! > This is intentional so that different users on Windows can have different
73//! > configuration and binaries. This also allows you to install SpacetimeDB on Windows
74//! > even if you are not a privileged user.
75//!
76//! ## Custom Root Directory
77//!
78//! Users on all platforms must be allowed to override the default installation
79//! paths entirely with a single `--root-dir` argument passed to the initial
80//! installation commands.
81//!
82//! If users specify a `--root-dir` flag, then the installation paths should be
83//! defined relative to the `root-dir` as follows:
84//!
85//! * `cli-config-dir`: `{root-dir}/config/`
86//! * `cli-bin-dir`: `{root-dir}/bin/`
87//! * `cli-bin-file`: `{root-dir}/spacetime[.exe]`
88//! * `data-dir`: `{root-dir}/data/`
89//!
90//! For reference, the below is an example installation using the `--root-dir` argument:
91//!
92//! ```sh
93//! {root-dir}
94//! ├── spacetime -> {root-dir}/bin/1.10.1/spacetimedb-update # Current, in $PATH
95//! ├── config
96//! │ ├── id_ecdsa # Private key
97//! │ ├── id_ecdsa.pub # Public key
98//! │ └── cli.toml # CLI configuration
99//! ├── bin
100//! | └── 1.10.1
101//! | ├── spacetimedb-update.exe # Version manager
102//! | ├── spacetimedb-cli.exe # CLI
103//! | ├── spacetimedb-standalone.exe # Server
104//! | ├── spacetimedb-cloud.exe # Server
105//! | ├── cli.default.toml # Template CLI configuration file
106//! | └── config.default.toml # Template server configuration file
107//! └── data/
108//! ```
109//!
110//! # Data directory structure
111//!
112//! The following is an example of the internal structure of data-dir. Note that this is not
113//! a stable hierarchy, and users should not rely on it being stable from version to version.
114//!
115//! ```sh
116//! {data-dir} # {Data}: CLI (--data-dir)
117//! ├── spacetime.pid # Lock file to prevent multiple instances, should be set to the pid of the running instance
118//! ├── config.toml # Server configuration (Human written, machine read only)
119//! ├── metadata.toml # Contains the edition, the MAJOR.MINOR.PATCH version number of the SpacetimeDB executable that created this directory. (Human readable, machine written only)
120//! ├── program-bytes # STANDALONE ONLY! Wasm modules aka `ProgramStorage` /var/lib/spacetime/data/standalone/2/program_bytes (NOTE: renamed from program_bytes)
121//! │ └── d6
122//! │ └── d9e66a8a285a416abd87e847c48b0990c6db6a5e0d5670c79a13f75dcabbe6
123//! ├── control-db # STANDALONE ONLY! Store information about the SpacetimeDB instances (NOTE: renamed from control_db)
124//! │ ├── blobs/ # Blobs storage
125//! │ ├── conf # Configuration for the Sled database
126//! │ └── db # Sled database
127//! ├── cache
128//! │ └── wasmtime
129//! ├── logs
130//! │ └── spacetimedb-standalone.2024-07-08.log # filename format: `spacetimedb-{edition}.YYYY-MM-DD.log`
131//! └── replicas
132//! ├── 1 # Database `replica_id`, unique per cluster
133//! │ ├── clog # `CommitLog` files
134//! │ │ └── 00000000000000000000.stdb.log
135//! │ ├── module_logs # Module logs
136//! │ │ └── 2024-07-08.log # filename format: `YYYY-MM-DD.log`
137//! │ └── snapshots # Snapshots of the database
138//! │ └── 00000000000000000000.snapshot_dir # BSATN-encoded `Snapshot`
139//! │ ├── 00000000000000000000.snapshot_bsatn
140//! │ └── objects # Objects storage
141//! │ └── 01
142//! │ └── 040a8585e6dc2c579c0c8f6017c7e6a0179a5d0410cd8db4b4affbd7d4d04f
143//! └── 34 # Database `replica_id`, unique per cluster
144//! ├── clog # `CommitLog` files
145//! │ └── 00000000000000000000.stdb.log
146//! ├── module_logs # Module logs
147//! │ └── 2024-07-08.log # filename format: `YYYY-MM-DD.log`
148//! └── snapshots # Snapshots of the database
149//! └── 00000000000000000000.snapshot_dir # BSATN-encoded `Snapshot`
150//! ├── 00000000000000000000.snapshot_bsatn
151//! └── objects # Objects storage directory trie
152//! └── 01
153//! └── 040a8585e6dc2c579c0c8f6017c7e6a0179a5d0410cd8db4b4affbd7d4d04f
154//! ```
155
156use crate::utils::PathBufExt;
157
158pub mod cli;
159pub mod server;
160pub mod standalone;
161mod utils;
162
163#[doc(hidden)]
164pub use serde as __serde;
165
166/// Implemented for path types. Use `from_path_unchecked()` to construct a strongly-typed
167/// path directly from a `PathBuf`.
168pub trait FromPathUnchecked {
169 /// The responsibility is on the caller to verify that the path is valid
170 /// for this directory structure node.
171 fn from_path_unchecked(path: impl Into<std::path::PathBuf>) -> Self;
172}
173
174path_type! {
175 /// The --root-dir for the spacetime installation, if specified.
176 // TODO: replace cfg(any()) with cfg(false) once stabilized
177 #[non_exhaustive(any())]
178 RootDir
179}
180
181impl RootDir {
182 pub fn cli_config_dir(&self) -> cli::ConfigDir {
183 cli::ConfigDir(self.0.join("config"))
184 }
185
186 pub fn cli_bin_file(&self) -> cli::BinFile {
187 cli::BinFile(self.0.join("spacetime").with_exe_ext())
188 }
189
190 pub fn cli_bin_dir(&self) -> cli::BinDir {
191 cli::BinDir(self.0.join("bin"))
192 }
193
194 pub fn data_dir(&self) -> server::ServerDataDir {
195 server::ServerDataDir(self.0.join("data"))
196 }
197
198 fn from_paths(paths: &SpacetimePaths) -> Option<Self> {
199 let SpacetimePaths {
200 cli_config_dir,
201 cli_bin_file,
202 cli_bin_dir,
203 data_dir,
204 } = paths;
205 let parent = cli_config_dir.0.parent()?;
206 let parents = [cli_bin_file.0.parent()?, cli_bin_dir.0.parent()?, data_dir.0.parent()?];
207 parents.iter().all(|x| *x == parent).then(|| Self(parent.to_owned()))
208 }
209}
210
211#[derive(Clone, Debug)]
212pub struct SpacetimePaths {
213 pub cli_config_dir: cli::ConfigDir,
214 pub cli_bin_file: cli::BinFile,
215 pub cli_bin_dir: cli::BinDir,
216 pub data_dir: server::ServerDataDir,
217}
218
219impl SpacetimePaths {
220 /// Get the default directories for the current platform.
221 ///
222 /// Returns an error if the platform director(y/ies) cannot be found.
223 pub fn platform_defaults() -> anyhow::Result<Self> {
224 #[cfg(windows)]
225 {
226 let data_dir = dirs::data_local_dir().ok_or_else(|| anyhow::anyhow!("Could not find LocalAppData"))?;
227 let root_dir = RootDir(data_dir.joined("SpacetimeDB"));
228 Ok(Self::from_root_dir(&root_dir))
229 }
230 #[cfg(not(windows))]
231 {
232 // `dirs` doesn't use XDG base dirs on macOS, which we want to do,
233 // so we use the `xdg` crate instead.
234 let base_dirs = xdg::BaseDirectories::with_prefix("spacetime")?;
235 // bin_home should really be in the xdg crate
236 let xdg_bin_home = std::env::var_os("XDG_BIN_HOME")
237 .map(std::path::PathBuf::from)
238 .filter(|p| p.is_absolute())
239 .unwrap_or_else(|| {
240 #[allow(deprecated)] // this is fine on non-windows platforms
241 std::env::home_dir().unwrap().joined(".local/bin")
242 });
243
244 let exe_name = "spacetime";
245
246 Ok(Self {
247 cli_config_dir: cli::ConfigDir(base_dirs.get_config_home()),
248 cli_bin_file: cli::BinFile(xdg_bin_home.join(exe_name)),
249 cli_bin_dir: cli::BinDir(base_dirs.get_data_file("bin")),
250 data_dir: server::ServerDataDir(base_dirs.get_data_file("data")),
251 })
252 }
253 }
254
255 pub fn from_root_dir(dir: &RootDir) -> Self {
256 Self {
257 cli_config_dir: dir.cli_config_dir(),
258 cli_bin_file: dir.cli_bin_file(),
259 cli_bin_dir: dir.cli_bin_dir(),
260 data_dir: dir.data_dir(),
261 }
262 }
263
264 pub fn to_root_dir(&self) -> Option<RootDir> {
265 RootDir::from_paths(self)
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use crate::{PathBufExt, RootDir, SpacetimePaths};
272 use std::path::Path;
273
274 #[cfg(not(windows))]
275 mod vars {
276 use std::ffi::{OsStr, OsString};
277 struct ResetVar<'a>(&'a str, Option<OsString>);
278 impl Drop for ResetVar<'_> {
279 fn drop(&mut self) {
280 maybe_set_var(self.0, self.1.as_deref())
281 }
282 }
283 fn maybe_set_var(var: &str, val: Option<impl AsRef<OsStr>>) {
284 if let Some(val) = val {
285 std::env::set_var(var, val);
286 } else {
287 std::env::remove_var(var);
288 }
289 }
290 pub(super) fn with_vars<const N: usize, R>(vars: [(&str, Option<&str>); N], f: impl FnOnce() -> R) -> R {
291 let _guard = vars.map(|(var, val)| {
292 let prev_val = std::env::var_os(var);
293 maybe_set_var(var, val);
294 ResetVar(var, prev_val)
295 });
296 f()
297 }
298 }
299
300 #[cfg(not(windows))]
301 #[test]
302 fn xdg() {
303 let p = Path::new;
304 let paths = vars::with_vars(
305 [
306 ("XDG_CONFIG_HOME", Some("/__config_home")),
307 ("XDG_DATA_HOME", Some("/__data_home")),
308 ("XDG_BIN_HOME", Some("/__bin_home")),
309 ],
310 SpacetimePaths::platform_defaults,
311 )
312 .unwrap();
313 assert_eq!(paths.cli_config_dir.0, p("/__config_home/spacetime"));
314 assert_eq!(paths.cli_bin_file.0, p("/__bin_home/spacetime"));
315 assert_eq!(paths.cli_bin_dir.0, p("/__data_home/spacetime/bin"));
316 assert_eq!(paths.data_dir.0, p("/__data_home/spacetime/data"));
317 }
318
319 #[cfg(windows)]
320 #[test]
321 fn windows() {
322 use crate::SpacetimePaths;
323
324 let paths = SpacetimePaths::platform_defaults().unwrap();
325 let appdata_local = dirs::data_local_dir().unwrap();
326 assert_eq!(paths.cli_config_dir.0, appdata_local.join("config"));
327 assert_eq!(paths.cli_bin_file.0, appdata_local.join("spacetime.exe"));
328 assert_eq!(paths.cli_bin_dir.0, appdata_local.join("bin"));
329 assert_eq!(paths.data_dir.0, appdata_local.join("data"));
330 }
331
332 #[test]
333 fn custom() {
334 let root = Path::new("/custom/path");
335 let paths = SpacetimePaths::from_root_dir(&RootDir(root.to_owned()));
336 assert_eq!(paths.cli_config_dir.0, root.join("config"));
337 assert_eq!(paths.cli_bin_file.0, root.join("spacetime").with_exe_ext());
338 assert_eq!(paths.cli_bin_dir.0, root.join("bin"));
339 assert_eq!(paths.data_dir.0, root.join("data"));
340 }
341}