dbx_tools_databricks_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, 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, Token>,
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}
39
40impl FileStore {
41 pub fn new(root: PathBuf) -> Result<Self> {
42 std::fs::create_dir_all(&root)?;
43 set_private_directory(&root)?;
44 Ok(Self {
45 token_cache: root.join("token-cache.json"),
46 root,
47 })
48 }
49
50 fn lock_path(&self, profile: &str) -> PathBuf {
51 self.root
52 .join("locks")
53 .join(format!("{}.lock", key_hash(profile)))
54 }
55
56 fn cache_lock_path(&self) -> PathBuf {
57 self.root.join("token-cache.lock")
58 }
59
60 pub(crate) async fn acquire_file_lock(
61 &self,
62 profile: &str,
63 timeout: Duration,
64 ) -> Result<FileLock> {
65 acquire_lock(self.lock_path(profile), profile.to_owned(), timeout).await
66 }
67
68 async fn acquire_cache_lock(&self) -> Result<FileLock> {
69 acquire_lock(
70 self.cache_lock_path(),
71 "token-cache.json".to_owned(),
72 Duration::from_secs(30),
73 )
74 .await
75 }
76
77 async fn read_cache(&self) -> Result<TokenCache> {
78 match tokio::fs::read(&self.token_cache).await {
79 Ok(raw) => {
80 let cache: TokenCache = serde_json::from_slice(&raw)?;
81 if cache.version != TOKEN_CACHE_VERSION {
82 return Err(Error::Storage(format!(
83 "token cache needs version {TOKEN_CACHE_VERSION}, got {}",
84 cache.version
85 )));
86 }
87 Ok(cache)
88 }
89 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(TokenCache::default()),
90 Err(error) => Err(error.into()),
91 }
92 }
93
94 async fn write_cache(&self, cache: &TokenCache) -> Result<()> {
95 let temporary = self
96 .root
97 .join(format!(".token-cache-{}.tmp", uuid::Uuid::new_v4()));
98 let raw = serde_json::to_vec_pretty(cache)?;
99 let mut file = tokio::fs::OpenOptions::new()
100 .create_new(true)
101 .write(true)
102 .open(&temporary)
103 .await?;
104 file.write_all(&raw).await?;
105 file.sync_all().await?;
106 drop(file);
107 set_private_file(&temporary)?;
108 tokio::fs::rename(&temporary, &self.token_cache).await?;
109 Ok(())
110 }
111}
112
113async fn acquire_lock(path: PathBuf, name: String, timeout: Duration) -> Result<FileLock> {
114 if let Some(parent) = path.parent() {
115 std::fs::create_dir_all(parent)?;
116 set_private_directory(parent)?;
117 }
118 tokio::task::spawn_blocking(move || {
119 let file = OpenOptions::new()
120 .create(true)
121 .truncate(false)
122 .read(true)
123 .write(true)
124 .open(path)?;
125 let deadline = Instant::now() + timeout;
126 loop {
127 if file.try_lock_exclusive()? {
128 return Ok(FileLock(file));
129 }
130 if Instant::now() >= deadline {
131 return Err(Error::LockTimeout(name));
132 }
133 std::thread::sleep(Duration::from_millis(50));
134 }
135 })
136 .await
137 .map_err(|error| Error::Storage(format!("file lock task failed: {error}")))?
138}
139
140pub(crate) struct FileLock(std::fs::File);
141impl StorageLock for FileLock {}
142
143impl Drop for FileLock {
144 fn drop(&mut self) {
145 let _ = fs4::fs_std::FileExt::unlock(&self.0);
146 }
147}
148
149#[async_trait]
150impl CredentialStore for FileStore {
151 async fn load(&self, profile: &str) -> Result<Option<Token>> {
152 let _lock = self.acquire_cache_lock().await?;
153 Ok(self.read_cache().await?.tokens.remove(profile))
154 }
155
156 async fn save(&self, profile: &str, token: &Token) -> Result<()> {
157 let _lock = self.acquire_cache_lock().await?;
158 let mut cache = self.read_cache().await?;
159 cache.tokens.insert(profile.to_owned(), token.clone());
160 self.write_cache(&cache).await
161 }
162
163 async fn delete(&self, profile: &str) -> Result<()> {
164 let _lock = self.acquire_cache_lock().await?;
165 let mut cache = self.read_cache().await?;
166 cache.tokens.remove(profile);
167 self.write_cache(&cache).await
168 }
169
170 async fn lock(&self, profile: &str, timeout: Duration) -> Result<Box<dyn StorageLock>> {
171 Ok(Box::new(self.acquire_file_lock(profile, timeout).await?))
172 }
173
174 fn name(&self) -> &'static str {
175 "file"
176 }
177}
178
179fn key_hash(profile: &str) -> String {
180 format!("{:x}", Sha256::digest(profile.as_bytes()))
181}
182
183#[cfg(unix)]
184fn set_private_file(path: &Path) -> Result<()> {
185 use std::os::unix::fs::PermissionsExt;
186 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
187 Ok(())
188}
189
190#[cfg(not(unix))]
191fn set_private_file(_path: &Path) -> Result<()> {
192 Ok(())
193}
194
195#[cfg(unix)]
196fn set_private_directory(path: &Path) -> Result<()> {
197 use std::os::unix::fs::PermissionsExt;
198 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
199 Ok(())
200}
201
202#[cfg(not(unix))]
203fn set_private_directory(_path: &Path) -> Result<()> {
204 Ok(())
205}