dbx_tools_auth/storage/
file.rs1use std::{
2 collections::HashMap,
3 fs::OpenOptions,
4 path::{Path, PathBuf},
5 time::{Duration, Instant},
6};
7
8use async_trait::async_trait;
9use fs4::fs_std::FileExt;
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use tokio::io::AsyncWriteExt;
13
14use super::{CredentialStore, FileLayout, StorageLock};
15use crate::{Error, Result, Token};
16
17const TOKEN_CACHE_VERSION: u8 = 1;
18
19#[derive(Serialize, Deserialize)]
20struct TokenCache {
21 version: u8,
22 #[serde(default)]
23 tokens: HashMap<String, serde_json::Value>,
24}
25
26impl Default for TokenCache {
27 fn default() -> Self {
28 Self {
29 version: TOKEN_CACHE_VERSION,
30 tokens: HashMap::new(),
31 }
32 }
33}
34
35pub struct FileStore {
36 root: PathBuf,
37 token_cache: PathBuf,
38 layout: FileLayout,
39}
40
41impl FileStore {
42 pub fn new(root: PathBuf) -> Result<Self> {
43 Self::with_layout(root, FileLayout::Single)
44 }
45
46 pub fn with_layout(root: PathBuf, layout: FileLayout) -> Result<Self> {
47 std::fs::create_dir_all(&root)?;
48 set_private_directory(&root)?;
49 Ok(Self {
50 token_cache: root.join("token-cache.json"),
51 root,
52 layout,
53 })
54 }
55
56 fn credential_store(&self, key: &str) -> Result<Self> {
57 Self::new(self.root.join(key_hash(key)))
58 }
59
60 fn cache_lock_path(&self) -> PathBuf {
61 self.root.join("token-cache.lock")
62 }
63
64 async fn acquire_cache_lock(&self) -> Result<FileLock> {
65 acquire_lock(
66 self.cache_lock_path(),
67 "token-cache.json".to_owned(),
68 Duration::from_secs(30),
69 )
70 .await
71 }
72
73 async fn read_cache(&self) -> Result<TokenCache> {
74 match tokio::fs::read(&self.token_cache).await {
75 Ok(raw) => {
76 let cache: TokenCache = serde_json::from_slice(&raw)?;
77 if cache.version != TOKEN_CACHE_VERSION {
78 return Err(Error::Storage(format!(
79 "token cache needs version {TOKEN_CACHE_VERSION}, got {}",
80 cache.version
81 )));
82 }
83 Ok(cache)
84 }
85 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(TokenCache::default()),
86 Err(error) => Err(error.into()),
87 }
88 }
89
90 async fn write_cache(&self, cache: &TokenCache) -> Result<()> {
91 let temporary = self
92 .root
93 .join(format!(".token-cache-{}.tmp", uuid::Uuid::new_v4()));
94 let raw = serde_json::to_vec_pretty(cache)?;
95 let mut options = tokio::fs::OpenOptions::new();
96 options.create_new(true).write(true);
97 #[cfg(unix)]
98 options.mode(0o600);
99 let mut file = options.open(&temporary).await?;
100 file.write_all(&raw).await?;
101 file.sync_all().await?;
102 drop(file);
103 set_private_file(&temporary)?;
104 tokio::fs::rename(&temporary, &self.token_cache).await?;
105 Ok(())
106 }
107}
108
109async fn acquire_lock(path: PathBuf, name: String, timeout: Duration) -> Result<FileLock> {
110 if let Some(parent) = path.parent() {
111 std::fs::create_dir_all(parent)?;
112 set_private_directory(parent)?;
113 }
114 tokio::task::spawn_blocking(move || {
115 let file = OpenOptions::new()
116 .create(true)
117 .truncate(false)
118 .read(true)
119 .write(true)
120 .open(path)?;
121 let deadline = Instant::now() + timeout;
122 loop {
123 if file.try_lock_exclusive()? {
124 return Ok(FileLock(file));
125 }
126 if Instant::now() >= deadline {
127 return Err(Error::LockTimeout(name));
128 }
129 std::thread::sleep(Duration::from_millis(50));
130 }
131 })
132 .await
133 .map_err(|error| Error::Storage(format!("file lock task failed: {error}")))?
134}
135
136pub(crate) struct FileLock(std::fs::File);
137impl StorageLock for FileLock {}
138
139impl Drop for FileLock {
140 fn drop(&mut self) {
141 let _ = fs4::fs_std::FileExt::unlock(&self.0);
142 }
143}
144
145#[async_trait]
146impl CredentialStore for FileStore {
147 async fn load(&self, profile: &str) -> Result<Option<Token>> {
148 if self.layout == FileLayout::PerCredential {
149 return self.credential_store(profile)?.load(profile).await;
150 }
151 let _lock = self.acquire_cache_lock().await?;
152 self.read_cache()
153 .await?
154 .tokens
155 .remove(profile)
156 .map(|value| serde_json::from_value(value).map_err(Into::into))
157 .transpose()
158 }
159
160 async fn save(&self, profile: &str, token: &Token) -> Result<()> {
161 if self.layout == FileLayout::PerCredential {
162 return self.credential_store(profile)?.save(profile, token).await;
163 }
164 let _lock = self.acquire_cache_lock().await?;
165 let mut cache = self.read_cache().await?;
166 cache
167 .tokens
168 .insert(profile.to_owned(), serde_json::to_value(token)?);
169 self.write_cache(&cache).await
170 }
171
172 async fn delete(&self, profile: &str) -> Result<()> {
173 if self.layout == FileLayout::PerCredential {
174 return self.credential_store(profile)?.delete(profile).await;
175 }
176 let _lock = self.acquire_cache_lock().await?;
177 let mut cache = self.read_cache().await?;
178 cache.tokens.remove(profile);
179 self.write_cache(&cache).await
180 }
181
182 async fn lock(&self, profile: &str, timeout: Duration) -> Result<Box<dyn StorageLock>> {
183 if self.layout == FileLayout::PerCredential {
184 return self.credential_store(profile)?.lock(profile, timeout).await;
185 }
186 Ok(Box::new(
187 acquire_lock(
188 self.root.join("token-cache.refresh.lock"),
189 "token-cache.json".to_owned(),
190 timeout,
191 )
192 .await?,
193 ))
194 }
195
196 fn name(&self) -> &'static str {
197 "file"
198 }
199}
200
201fn key_hash(profile: &str) -> String {
202 format!("{:x}", Sha256::digest(profile.as_bytes()))
203}
204
205#[cfg(unix)]
206fn set_private_file(path: &Path) -> Result<()> {
207 use std::os::unix::fs::PermissionsExt;
208 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
209 Ok(())
210}
211
212#[cfg(not(unix))]
213fn set_private_file(_path: &Path) -> Result<()> {
214 Ok(())
215}
216
217#[cfg(unix)]
218fn set_private_directory(path: &Path) -> Result<()> {
219 use std::os::unix::fs::PermissionsExt;
220 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
221 Ok(())
222}
223
224#[cfg(not(unix))]
225fn set_private_directory(_path: &Path) -> Result<()> {
226 Ok(())
227}