Skip to main content

tuitbot_core/startup/
db.rs

1//! Token file I/O and filesystem path helpers.
2
3use std::path::PathBuf;
4
5use super::config::{StartupError, StoredTokens};
6
7// ============================================================================
8// Token File I/O
9// ============================================================================
10
11/// Default directory for Tuitbot data files (`~/.tuitbot/`).
12pub fn data_dir() -> PathBuf {
13    dirs::home_dir()
14        .unwrap_or_else(|| PathBuf::from("."))
15        .join(".tuitbot")
16}
17
18/// Path to the token storage file (`~/.tuitbot/tokens.json`).
19pub fn token_file_path() -> PathBuf {
20    data_dir().join("tokens.json")
21}
22
23/// Load OAuth tokens from the default file path.
24pub fn load_tokens_from_file() -> Result<StoredTokens, StartupError> {
25    let path = token_file_path();
26    let contents = std::fs::read_to_string(&path).map_err(|e| {
27        if e.kind() == std::io::ErrorKind::NotFound {
28            StartupError::AuthRequired
29        } else {
30            StartupError::Io(e)
31        }
32    })?;
33    serde_json::from_str(&contents)
34        .map_err(|e| StartupError::Other(format!("failed to parse tokens file: {e}")))
35}
36
37/// Save OAuth tokens to the default file path with secure permissions.
38///
39/// Creates the `~/.tuitbot/` directory if it does not exist.
40/// On Unix, sets file permissions to 0600 (owner read/write only).
41pub fn save_tokens_to_file(tokens: &StoredTokens) -> Result<(), StartupError> {
42    let dir = data_dir();
43    std::fs::create_dir_all(&dir)?;
44
45    let path = token_file_path();
46    let json = serde_json::to_string_pretty(tokens)
47        .map_err(|e| StartupError::Other(format!("failed to serialize tokens: {e}")))?;
48    std::fs::write(&path, json)?;
49
50    // Set file permissions to 0600 on Unix (owner read/write only).
51    #[cfg(unix)]
52    {
53        use std::os::unix::fs::PermissionsExt;
54        let perms = std::fs::Permissions::from_mode(0o600);
55        std::fs::set_permissions(&path, perms)?;
56    }
57
58    Ok(())
59}
60
61// ============================================================================
62// Path Helpers
63// ============================================================================
64
65/// Expand `~` at the start of a path to the user's home directory.
66pub fn expand_tilde(path: &str) -> PathBuf {
67    if let Some(rest) = path.strip_prefix("~/") {
68        if let Some(home) = dirs::home_dir() {
69            return home.join(rest);
70        }
71    } else if path == "~" {
72        if let Some(home) = dirs::home_dir() {
73            return home;
74        }
75    }
76    PathBuf::from(path)
77}
78
79/// Resolve the database path by loading the config file and reading `storage.db_path`.
80///
81/// Falls back to `~/.tuitbot/tuitbot.db` if the config cannot be loaded.
82/// Returns an error if the resolved `db_path` is empty, whitespace-only,
83/// or points to an existing directory.
84pub fn resolve_db_path(config_path: &str) -> Result<PathBuf, crate::error::ConfigError> {
85    use crate::config::Config;
86    let config = match Config::load(Some(config_path)) {
87        Ok(c) => c,
88        Err(_) => return Ok(data_dir().join("tuitbot.db")),
89    };
90
91    validate_db_path(&config.storage.db_path)
92}
93
94/// Validate and expand a `storage.db_path` value.
95///
96/// Rejects empty, whitespace-only, and directory paths with a clear error.
97pub fn validate_db_path(raw: &str) -> Result<PathBuf, crate::error::ConfigError> {
98    let trimmed = raw.trim();
99    if trimmed.is_empty() {
100        return Err(crate::error::ConfigError::InvalidValue {
101            field: "storage.db_path".to_string(),
102            message: "must not be empty or whitespace-only".to_string(),
103        });
104    }
105    let expanded = expand_tilde(trimmed);
106    if expanded.is_dir() {
107        return Err(crate::error::ConfigError::InvalidValue {
108            field: "storage.db_path".to_string(),
109            message: format!("'{}' is a directory, must point to a file", trimmed),
110        });
111    }
112    Ok(expanded)
113}