Skip to main content

origin_platform/
paths.rs

1//! Where an application keeps its data.
2//!
3//! Computed here rather than taken from the desktop host, so that a GUI run and a
4//! headless run of the same product agree by construction. Two code paths deriving the
5//! same directory independently is how a headless mode ends up looking at an empty
6//! database.
7
8use origin_domain::{AppError, Result};
9use std::path::PathBuf;
10
11/// Overrides the location entirely. Used by tests, and by anyone running a portable
12/// installation.
13pub const DATA_DIR_ENV: &str = "ORIGIN_DATA_DIR";
14
15/// The directory for `app_id`, created if it does not exist.
16///
17/// | Platform | Location |
18/// | --- | --- |
19/// | macOS | `~/Library/Application Support/<app_id>` |
20/// | Windows | `%APPDATA%\<app_id>` |
21/// | Linux | `$XDG_DATA_HOME/<app_id>`, else `~/.local/share/<app_id>` |
22pub fn data_dir(app_id: &str) -> Result<PathBuf> {
23    let directory = resolve(
24        app_id,
25        std::env::var_os(DATA_DIR_ENV).map(PathBuf::from),
26        base_dir()?,
27    );
28
29    std::fs::create_dir_all(&directory).map_err(|error| {
30        AppError::storage(format!("cannot create {}: {error}", directory.display()))
31    })?;
32
33    Ok(directory)
34}
35
36/// The location decision, without touching the environment or the filesystem.
37fn resolve(app_id: &str, override_path: Option<PathBuf>, base: PathBuf) -> PathBuf {
38    override_path.unwrap_or_else(|| base.join(app_id))
39}
40
41#[cfg(target_os = "macos")]
42fn base_dir() -> Result<PathBuf> {
43    Ok(home()?.join("Library").join("Application Support"))
44}
45
46#[cfg(target_os = "windows")]
47fn base_dir() -> Result<PathBuf> {
48    std::env::var_os("APPDATA")
49        .map(PathBuf::from)
50        .ok_or_else(|| AppError::configuration("APPDATA is not set"))
51}
52
53#[cfg(not(any(target_os = "macos", target_os = "windows")))]
54fn base_dir() -> Result<PathBuf> {
55    match std::env::var_os("XDG_DATA_HOME") {
56        Some(path) => Ok(PathBuf::from(path)),
57        None => Ok(home()?.join(".local").join("share")),
58    }
59}
60
61#[cfg(not(target_os = "windows"))]
62fn home() -> Result<PathBuf> {
63    std::env::var_os("HOME")
64        .map(PathBuf::from)
65        .ok_or_else(|| AppError::configuration("HOME is not set"))
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn an_override_replaces_the_location_entirely() {
74        let resolved = resolve(
75            "dev.origin.test",
76            Some(PathBuf::from("/tmp/portable")),
77            PathBuf::from("/home/user/.local/share"),
78        );
79
80        assert_eq!(resolved, PathBuf::from("/tmp/portable"));
81    }
82
83    #[test]
84    fn the_default_location_carries_the_application_id() {
85        let resolved = resolve("dev.origin.test", None, PathBuf::from("/base"));
86
87        assert_eq!(resolved, PathBuf::from("/base/dev.origin.test"));
88    }
89
90    #[test]
91    fn resolving_creates_the_directory() {
92        let target = std::env::temp_dir().join(format!("origin-paths-{}", std::process::id()));
93        let _ = std::fs::remove_dir_all(&target);
94
95        let resolved = data_dir(target.to_str().expect("utf-8 path"));
96
97        // Without an override the id is appended to the platform base directory, so
98        // this only asserts that whatever came back exists.
99        assert!(resolved.expect("resolve").is_dir());
100    }
101}