Skip to main content

bwx/
dirs.rs

1use crate::prelude::*;
2
3use std::os::unix::fs::{DirBuilderExt as _, PermissionsExt as _};
4
5pub fn make_all() -> Result<()> {
6    create_dir_all_with_permissions(&config_dir(), 0o700)?;
7    create_dir_all_with_permissions(&cache_dir(), 0o700)?;
8    create_dir_all_with_permissions(&runtime_dir(), 0o700)?;
9    create_dir_all_with_permissions(&data_dir(), 0o700)?;
10
11    Ok(())
12}
13
14fn create_dir_all_with_permissions(
15    path: &std::path::Path,
16    mode: u32,
17) -> Result<()> {
18    // ensure the initial directory creation happens with the correct mode,
19    // to avoid race conditions
20    std::fs::DirBuilder::new()
21        .recursive(true)
22        .mode(mode)
23        .create(path)
24        .map_err(|source| Error::CreateDirectory {
25            source,
26            file: path.to_path_buf(),
27        })?;
28    // but also make sure to forcibly set the mode, in case the directory
29    // already existed
30    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
31        .map_err(|source| Error::CreateDirectory {
32            source,
33            file: path.to_path_buf(),
34        })?;
35    Ok(())
36}
37
38pub fn config_file() -> std::path::PathBuf {
39    config_dir().join("config.json")
40}
41
42pub fn db_file(server: &str, email: &str) -> std::path::PathBuf {
43    let server = urlencoding::encode(server).into_owned();
44    cache_dir().join(format!("{server}:{email}.json"))
45}
46
47pub fn pid_file() -> std::path::PathBuf {
48    runtime_dir().join("pidfile")
49}
50
51pub fn agent_stdout_file() -> std::path::PathBuf {
52    data_dir().join("agent.out")
53}
54
55pub fn agent_stderr_file() -> std::path::PathBuf {
56    data_dir().join("agent.err")
57}
58
59pub fn device_id_file() -> std::path::PathBuf {
60    data_dir().join("device_id")
61}
62
63pub fn socket_file() -> std::path::PathBuf {
64    runtime_dir().join("socket")
65}
66
67pub fn ssh_agent_socket_file() -> std::path::PathBuf {
68    runtime_dir().join("ssh-agent-socket")
69}
70
71fn home_dir() -> std::path::PathBuf {
72    std::env::var_os("HOME").map_or_else(
73        || std::path::PathBuf::from("/"),
74        std::path::PathBuf::from,
75    )
76}
77
78#[cfg(target_os = "macos")]
79fn config_dir() -> std::path::PathBuf {
80    home_dir()
81        .join("Library/Application Support")
82        .join(profile())
83}
84
85#[cfg(target_os = "macos")]
86fn cache_dir() -> std::path::PathBuf {
87    home_dir().join("Library/Caches").join(profile())
88}
89
90#[cfg(target_os = "macos")]
91fn data_dir() -> std::path::PathBuf {
92    config_dir()
93}
94
95#[cfg(not(target_os = "macos"))]
96fn xdg_or(env: &str, fallback_rel: &str) -> std::path::PathBuf {
97    std::env::var_os(env)
98        .filter(|v| std::path::Path::new(v).is_absolute())
99        .map_or_else(
100            || home_dir().join(fallback_rel),
101            std::path::PathBuf::from,
102        )
103}
104
105#[cfg(not(target_os = "macos"))]
106fn config_dir() -> std::path::PathBuf {
107    xdg_or("XDG_CONFIG_HOME", ".config").join(profile())
108}
109
110#[cfg(not(target_os = "macos"))]
111fn cache_dir() -> std::path::PathBuf {
112    xdg_or("XDG_CACHE_HOME", ".cache").join(profile())
113}
114
115#[cfg(not(target_os = "macos"))]
116fn data_dir() -> std::path::PathBuf {
117    xdg_or("XDG_DATA_HOME", ".local/share").join(profile())
118}
119
120fn runtime_dir() -> std::path::PathBuf {
121    // Honor XDG_RUNTIME_DIR on all platforms when explicitly set. macOS has
122    // no native equivalent, but respecting the override lets tests and
123    // advanced users isolate per-instance sockets; falls through to a
124    // $TMPDIR-based path when unset.
125    if let Some(d) = std::env::var_os("XDG_RUNTIME_DIR") {
126        if std::path::Path::new(&d).is_absolute() {
127            return std::path::PathBuf::from(d).join(profile());
128        }
129    }
130    format!(
131        "{}/{}-{}",
132        std::env::temp_dir().to_string_lossy(),
133        profile(),
134        rustix::process::getuid().as_raw()
135    )
136    .into()
137}
138
139pub fn profile() -> String {
140    match std::env::var("BWX_PROFILE") {
141        Ok(profile) if !profile.is_empty() => format!("bwx-{profile}"),
142        _ => "bwx".to_string(),
143    }
144}