Skip to main content

storekit/
paths.rs

1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub struct App {
5    pub folder: &'static str,
6    pub env_prefix: &'static str,
7}
8
9impl App {
10    pub fn folder(self) -> String {
11        if cfg!(debug_assertions) {
12            format!("{}-dev", self.folder)
13        } else {
14            self.folder.to_owned()
15        }
16    }
17
18    pub fn config_dir(self) -> PathBuf {
19        dirs::config_dir()
20            .unwrap_or_else(|| home_dir().join(".config"))
21            .join(self.folder())
22    }
23
24    pub fn data_dir(self) -> PathBuf {
25        std::env::var(self.env_key("DATA"))
26            .map(PathBuf::from)
27            .unwrap_or_else(|_| {
28                dirs::data_dir()
29                    .unwrap_or_else(|| home_dir().join(".local").join("share"))
30                    .join(self.folder())
31            })
32    }
33
34    pub fn db_path(self, file: impl AsRef<Path>) -> PathBuf {
35        std::env::var(self.env_key("DB"))
36            .map(PathBuf::from)
37            .unwrap_or_else(|_| self.data_dir().join(file))
38    }
39
40    pub fn token_path(self) -> PathBuf {
41        self.config_dir().join("token")
42    }
43
44    pub fn mcp_token_path(self) -> PathBuf {
45        self.data_dir().join("mcp-token")
46    }
47
48    pub fn crash_spool_dir(self) -> PathBuf {
49        self.data_dir().join("crash")
50    }
51
52    pub fn traces_dir(self) -> PathBuf {
53        self.data_dir().join("traces")
54    }
55
56    pub(crate) fn env_key(self, suffix: &str) -> String {
57        format!("{}_{suffix}", self.env_prefix)
58    }
59}
60
61pub fn tilde(path: &str) -> String {
62    let home = home_dir().to_string_lossy().into_owned();
63    match path.strip_prefix(&home) {
64        Some(rest) if !home.is_empty() => format!("~{rest}"),
65        _ => path.to_owned(),
66    }
67}
68
69#[cfg(unix)]
70pub fn set_owner_only_permissions(path: &Path) {
71    use std::os::unix::fs::PermissionsExt;
72
73    if let Ok(metadata) = std::fs::metadata(path) {
74        let mut permissions = metadata.permissions();
75        permissions.set_mode(0o600);
76        let _ = std::fs::set_permissions(path, permissions);
77    }
78}
79
80#[cfg(not(unix))]
81pub fn set_owner_only_permissions(_path: &Path) {}
82
83fn home_dir() -> PathBuf {
84    dirs::home_dir()
85        .or_else(|| std::env::var("HOME").ok().map(PathBuf::from))
86        .expect("no home directory on this machine")
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    const TEST_APP: App = App {
94        folder: "storekit-path-test",
95        env_prefix: "PITO_STOREKIT_PATH_TEST",
96    };
97
98    #[test]
99    fn the_app_dirs_end_in_the_apps_own_name() {
100        assert!(TEST_APP.config_dir().ends_with(TEST_APP.folder()));
101        assert!(
102            TEST_APP
103                .token_path()
104                .ends_with(format!("{}/token", TEST_APP.folder()))
105        );
106    }
107
108    #[test]
109    fn a_debug_build_never_names_the_release_folder() {
110        assert_eq!(TEST_APP.folder(), "storekit-path-test-dev");
111        assert!(!TEST_APP.config_dir().ends_with(TEST_APP.folder));
112    }
113
114    #[cfg(target_os = "linux")]
115    #[test]
116    fn on_linux_the_paths_stay_exactly_where_existing_stores_live() {
117        assert_eq!(
118            TEST_APP.config_dir(),
119            home_dir().join(".config").join(TEST_APP.folder())
120        );
121        if std::env::var(TEST_APP.env_key("DATA")).is_err() {
122            assert_eq!(
123                TEST_APP.data_dir(),
124                home_dir()
125                    .join(".local")
126                    .join("share")
127                    .join(TEST_APP.folder())
128            );
129        }
130    }
131}