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