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}