tuitbot_core/startup/
db.rs1use std::path::PathBuf;
4
5use super::config::{StartupError, StoredTokens};
6
7pub fn data_dir() -> PathBuf {
13 dirs::home_dir()
14 .unwrap_or_else(|| PathBuf::from("."))
15 .join(".tuitbot")
16}
17
18pub fn token_file_path() -> PathBuf {
20 data_dir().join("tokens.json")
21}
22
23pub 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
37pub 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 #[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
61pub 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
79pub 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
94pub 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}