1use std::{
5 fs::{self, File},
6 io::Write,
7 path::{Path, PathBuf},
8};
9use crate::{
11 _prelude::*,
12 auth::{ScopeSet, TokenFamily, TokenRecord, TokenSecret},
13 store::{BrokerStore, CompareAndSwapOutcome, StoreError, StoreFuture, StoreKey},
14};
15
16#[derive(Clone, Debug)]
18pub struct FileStore {
19 path: PathBuf,
20 inner: Arc<RwLock<HashMap<StoreKey, TokenRecord>>>,
21}
22impl FileStore {
23 pub fn open(path: impl Into<PathBuf>) -> Result<Self, StoreError> {
25 let path = path.into();
26
27 Self::ensure_parent_exists(&path)?;
28
29 let snapshot = if path.exists() { Self::load_snapshot(&path)? } else { HashMap::new() };
30
31 Ok(Self { path, inner: Arc::new(RwLock::new(snapshot)) })
32 }
33
34 fn load_snapshot(path: &Path) -> Result<HashMap<StoreKey, TokenRecord>, StoreError> {
35 if !path.exists() {
36 return Ok(HashMap::new());
37 }
38
39 let metadata = path.metadata().map_err(|e| StoreError::Backend {
40 message: format!("Failed to inspect {}: {e}", path.display()),
41 })?;
42
43 if metadata.len() == 0 {
44 return Ok(HashMap::new());
45 }
46
47 let bytes = fs::read(path).map_err(|e| StoreError::Backend {
48 message: format!("Failed to read {}: {e}", path.display()),
49 })?;
50
51 let entries: Vec<(StoreKey, TokenRecord)> =
52 serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization {
53 message: format!("Failed to parse {}: {e}", path.display()),
54 })?;
55
56 Ok(entries.into_iter().collect())
57 }
58
59 fn ensure_parent_exists(path: &Path) -> Result<(), StoreError> {
60 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
61 fs::create_dir_all(parent).map_err(|e| StoreError::Backend {
62 message: format!("Failed to create store directory {}: {e}", parent.display()),
63 })?;
64 }
65 Ok(())
66 }
67
68 fn persist_locked(&self, contents: &HashMap<StoreKey, TokenRecord>) -> Result<(), StoreError> {
69 Self::ensure_parent_exists(&self.path)?;
70
71 let snapshot: Vec<_> = contents.iter().collect();
72 let serialized =
73 serde_json::to_vec_pretty(&snapshot).map_err(|e| StoreError::Serialization {
74 message: format!("Failed to serialize store snapshot: {e}"),
75 })?;
76 let mut tmp_path = self.path.clone();
77
78 tmp_path.set_extension("tmp");
79
80 {
81 let mut file = File::create(&tmp_path).map_err(|e| StoreError::Backend {
82 message: format!("Failed to create {}: {e}", tmp_path.display()),
83 })?;
84
85 file.write_all(&serialized).map_err(|e| StoreError::Backend {
86 message: format!("Failed to write {}: {e}", tmp_path.display()),
87 })?;
88 file.sync_all().map_err(|e| StoreError::Backend {
89 message: format!("Failed to sync {}: {e}", tmp_path.display()),
90 })?;
91 }
92
93 fs::rename(&tmp_path, &self.path).map_err(|e| StoreError::Backend {
94 message: format!("Failed to replace {}: {e}", self.path.display()),
95 })
96 }
97
98 fn make_key(family: &TokenFamily, scope: &ScopeSet) -> StoreKey {
99 StoreKey::new(family, scope)
100 }
101
102 fn refresh_matches(current: Option<&TokenSecret>, expected: Option<&str>) -> bool {
103 match (current.map(TokenSecret::expose), expected) {
104 (None, None) => true,
105 (Some(cur), Some(exp)) => cur == exp,
106 _ => false,
107 }
108 }
109}
110impl BrokerStore for FileStore {
111 fn save(&self, record: TokenRecord) -> StoreFuture<'_, ()> {
112 Box::pin(async move {
113 let key = Self::make_key(&record.family, &record.scope);
114 let mut guard = self.inner.write();
115
116 guard.insert(key, record);
117 self.persist_locked(&guard)?;
118
119 Ok(())
120 })
121 }
122
123 fn fetch<'a>(
124 &'a self,
125 family: &'a TokenFamily,
126 scope: &'a ScopeSet,
127 ) -> StoreFuture<'a, Option<TokenRecord>> {
128 Box::pin(async move {
129 let key = Self::make_key(family, scope);
130
131 Ok(self.inner.read().get(&key).cloned())
132 })
133 }
134
135 fn compare_and_swap_refresh<'a>(
136 &'a self,
137 family: &'a TokenFamily,
138 scope: &'a ScopeSet,
139 expected_refresh: Option<&'a str>,
140 replacement: TokenRecord,
141 ) -> StoreFuture<'a, CompareAndSwapOutcome> {
142 Box::pin(async move {
143 let key = Self::make_key(family, scope);
144 let mut guard = self.inner.write();
145 let outcome = match guard.get(&key) {
146 Some(existing)
147 if Self::refresh_matches(existing.refresh_token.as_ref(), expected_refresh) =>
148 CompareAndSwapOutcome::Updated,
149 Some(_) => CompareAndSwapOutcome::RefreshMismatch,
150 None => CompareAndSwapOutcome::Missing,
151 };
152
153 if matches!(outcome, CompareAndSwapOutcome::Updated) {
154 guard.insert(key, replacement);
155 self.persist_locked(&guard)?;
156 }
157
158 Ok(outcome)
159 })
160 }
161
162 fn revoke<'a>(
163 &'a self,
164 family: &'a TokenFamily,
165 scope: &'a ScopeSet,
166 instant: OffsetDateTime,
167 ) -> StoreFuture<'a, Option<TokenRecord>> {
168 Box::pin(async move {
169 let key = Self::make_key(family, scope);
170 let mut guard = self.inner.write();
171 let result = match guard.get_mut(&key) {
172 Some(record) => {
173 record.revoke(instant);
174
175 let cloned = record.clone();
176
177 self.persist_locked(&guard)?;
178
179 Some(cloned)
180 },
181 None => None,
182 };
183
184 Ok(result)
185 })
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use std::{env, process};
193 use tokio::runtime::Runtime;
195 use super::*;
197 use crate::auth::{PrincipalId, TenantId};
198
199 fn temp_path() -> PathBuf {
200 let unique = format!(
201 "oauth2_broker_file_store_{}_{}.json",
202 process::id(),
203 OffsetDateTime::now_utc().unix_timestamp_nanos(),
204 );
205
206 env::temp_dir().join(unique)
207 }
208
209 fn build_record() -> (TokenFamily, ScopeSet, TokenRecord) {
210 let tenant = TenantId::new("tenant-demo").expect("Failed to build tenant fixture.");
211 let principal =
212 PrincipalId::new("principal-demo").expect("Failed to build principal fixture.");
213 let scope = ScopeSet::new(["tweet.read"]).expect("Failed to build scope fixture.");
214 let family = TokenFamily::new(tenant, principal);
215 let record = TokenRecord::builder(family.clone(), scope.clone())
216 .access_token("access-token")
217 .expires_in(Duration::hours(1))
218 .build()
219 .expect("Failed to build file-store test record.");
220
221 (family, scope, record)
222 }
223
224 #[test]
225 fn save_and_reload_round_trip() {
226 let path = temp_path();
227 let store = FileStore::open(&path).expect("Failed to open file store snapshot.");
228 let (family, scope, record) = build_record();
229 let rt = Runtime::new().expect("Failed to build Tokio runtime for file store test.");
230
231 rt.block_on(store.save(record.clone()))
232 .expect("Failed to save fixture record to file store.");
233 drop(store);
234
235 let reopened = FileStore::open(&path).expect("Failed to reopen file store snapshot.");
236 let fetched = rt
237 .block_on(reopened.fetch(&family, &scope))
238 .expect("Failed to fetch fixture record from file store.")
239 .expect("File store lost record after reopen.");
240
241 assert_eq!(fetched.access_token.expose(), record.access_token.expose());
242
243 fs::remove_file(&path).unwrap_or_else(|e| {
244 panic!("Failed to remove temporary file store snapshot {}: {e}", path.display())
245 });
246 }
247}