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}