1use std::fs::OpenOptions;
13use std::path::{Path, PathBuf};
14use std::time::{Duration, Instant};
15
16use serde_json::{Map, Value};
17
18pub const FILE: &str = "oauth_sessions.json";
19
20pub fn path() -> PathBuf {
22 std::env::home_dir().unwrap_or_default().join(".dre").join(FILE)
23}
24
25pub fn load(key: &str) -> Option<Value> {
27 read(&path()).remove(key)
28}
29
30pub fn store(key: &str, session: Option<Value>) -> std::io::Result<()> {
32 store_at(&path(), key, session)
33}
34
35fn store_at(path: &Path, key: &str, session: Option<Value>) -> std::io::Result<()> {
36 if let Some(dir) = path.parent() {
37 std::fs::create_dir_all(dir)?;
38 }
39 let _lock = Lock::acquire(&path.with_extension("json.lock"));
40 let mut all = read(path);
41 match session {
42 Some(s) => all.insert(key.to_string(), s),
43 None => all.remove(key),
44 };
45 let bytes = serde_json::to_vec_pretty(&Value::Object(all)).unwrap_or_default();
46 crate::util::write_atomic_mode(path, &bytes, Some(0o600))
47}
48
49fn read(path: &Path) -> Map<String, Value> {
51 std::fs::read_to_string(path)
52 .ok()
53 .and_then(|t| serde_json::from_str(&t).ok())
54 .unwrap_or_default()
55}
56
57struct Lock(Option<PathBuf>);
60
61impl Lock {
62 fn acquire(path: &Path) -> Lock {
63 let started = Instant::now();
64 loop {
65 if OpenOptions::new().write(true).create_new(true).open(path).is_ok() {
66 return Lock(Some(path.to_path_buf()));
67 }
68 let stale = std::fs::metadata(path)
69 .and_then(|m| m.modified())
70 .ok()
71 .and_then(|t| t.elapsed().ok())
72 .is_some_and(|age| age > Duration::from_secs(10));
73 if stale {
74 let _ = std::fs::remove_file(path);
75 continue;
76 }
77 if started.elapsed() > Duration::from_secs(10) {
78 return Lock(None);
80 }
81 std::thread::sleep(Duration::from_millis(20));
82 }
83 }
84}
85
86impl Drop for Lock {
87 fn drop(&mut self) {
88 if let Some(p) = &self.0 {
89 let _ = std::fs::remove_file(p);
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use serde_json::json;
98
99 #[test]
100 fn concurrent_logins_keep_every_entry_and_the_file_is_private() {
101 let dir = tempfile::tempdir().unwrap();
102 let path = dir.path().join(".dre").join(FILE);
103 std::thread::scope(|s| {
104 for i in 0..8 {
105 let path = &path;
106 s.spawn(move || {
107 store_at(path, &format!("databricks/ws-{i}/c"), Some(json!({"n": i}))).unwrap()
108 });
109 }
110 });
111 let all = read(&path);
112 assert_eq!(all.len(), 8);
113 assert_eq!(all["databricks/ws-3/c"], json!({"n": 3}));
114 store_at(&path, "databricks/ws-3/c", None).unwrap();
115 assert!(!read(&path).contains_key("databricks/ws-3/c"));
116 assert!(!path.with_extension("json.lock").exists());
117 #[cfg(unix)]
118 {
119 use std::os::unix::fs::PermissionsExt;
120 let mode = std::fs::metadata(&path).unwrap().permissions().mode();
121 assert_eq!(mode & 0o777, 0o600);
122 }
123 }
124}