spotify_cli/storage/
unified_token.rs1use thiserror::Error;
11use tracing::{debug, warn};
12
13use super::config::TokenStorageBackend;
14use super::keyring::{KeyringError, KeyringStore};
15use super::token_store::{TokenStore, TokenStoreError};
16use crate::oauth::token::Token;
17
18#[derive(Debug, Error)]
20pub enum UnifiedTokenError {
21 #[error("File storage error: {0}")]
22 File(#[from] TokenStoreError),
23
24 #[error("Keyring error: {0}")]
25 Keyring(#[from] KeyringError),
26
27 #[error("Token not found")]
28 NotFound,
29}
30
31pub struct UnifiedTokenStore {
33 backend: TokenStorageBackend,
34 keyring: Option<KeyringStore>,
35 file: TokenStore,
36}
37
38impl UnifiedTokenStore {
39 pub fn new(preferred_backend: TokenStorageBackend) -> Result<Self, UnifiedTokenError> {
43 let file = TokenStore::new()?;
44
45 let (backend, keyring) = match preferred_backend {
46 TokenStorageBackend::Keyring => match KeyringStore::new() {
47 Ok(store) => {
48 debug!("Using keyring for token storage");
49 (TokenStorageBackend::Keyring, Some(store))
50 }
51 Err(e) => {
52 warn!("Keyring unavailable, falling back to file storage: {}", e);
53 (TokenStorageBackend::File, None)
54 }
55 },
56 TokenStorageBackend::File => {
57 debug!("Using file for token storage (configured)");
58 (TokenStorageBackend::File, None)
59 }
60 };
61
62 Ok(Self {
63 backend,
64 keyring,
65 file,
66 })
67 }
68
69 pub fn save(&self, token: &Token) -> Result<(), UnifiedTokenError> {
71 match self.backend {
72 TokenStorageBackend::Keyring => {
73 if let Some(ref keyring) = self.keyring {
74 keyring.save(token)?;
75 debug!("Token saved to keyring");
76 Ok(())
77 } else {
78 self.file.save(token)?;
80 debug!("Token saved to file (keyring fallback)");
81 Ok(())
82 }
83 }
84 TokenStorageBackend::File => {
85 self.file.save(token)?;
86 debug!("Token saved to file");
87 Ok(())
88 }
89 }
90 }
91
92 pub fn load(&self) -> Result<Token, UnifiedTokenError> {
96 match self.backend {
97 TokenStorageBackend::Keyring => {
98 if let Some(ref keyring) = self.keyring {
99 match keyring.load() {
100 Ok(token) => {
101 debug!("Token loaded from keyring");
102 Ok(token)
103 }
104 Err(KeyringError::NotFound) => {
105 debug!("Token not in keyring, checking file for migration");
107 match self.file.load() {
108 Ok(token) => {
109 debug!("Found token in file, migrating to keyring");
110 if keyring.save(&token).is_ok() {
112 let _ = self.file.delete();
114 debug!("Token migrated from file to keyring");
115 }
116 Ok(token)
117 }
118 Err(TokenStoreError::NotFound) => Err(UnifiedTokenError::NotFound),
119 Err(e) => Err(UnifiedTokenError::File(e)),
120 }
121 }
122 Err(e) => Err(UnifiedTokenError::Keyring(e)),
123 }
124 } else {
125 self.file.load().map_err(|e| match e {
127 TokenStoreError::NotFound => UnifiedTokenError::NotFound,
128 other => UnifiedTokenError::File(other),
129 })
130 }
131 }
132 TokenStorageBackend::File => self.file.load().map_err(|e| match e {
133 TokenStoreError::NotFound => UnifiedTokenError::NotFound,
134 other => UnifiedTokenError::File(other),
135 }),
136 }
137 }
138
139 pub fn delete(&self) -> Result<(), UnifiedTokenError> {
143 if let Some(ref keyring) = self.keyring {
145 keyring.delete()?;
146 debug!("Token deleted from keyring");
147 }
148
149 self.file.delete()?;
151 debug!("Token deleted from file");
152
153 Ok(())
154 }
155
156 pub fn exists(&self) -> bool {
158 match self.backend {
159 TokenStorageBackend::Keyring => {
160 if let Some(ref keyring) = self.keyring {
161 keyring.exists() || self.file.exists()
162 } else {
163 self.file.exists()
164 }
165 }
166 TokenStorageBackend::File => self.file.exists(),
167 }
168 }
169
170 pub fn backend(&self) -> TokenStorageBackend {
172 self.backend
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn unified_token_error_display() {
182 let err = UnifiedTokenError::NotFound;
183 let display = format!("{}", err);
184 assert!(display.contains("not found"));
185 }
186
187 #[test]
188 fn file_backend_creates_successfully() {
189 let store = UnifiedTokenStore::new(TokenStorageBackend::File);
190 assert!(store.is_ok());
191 assert_eq!(store.unwrap().backend(), TokenStorageBackend::File);
192 }
193
194 #[test]
195 fn exists_returns_false_when_empty() {
196 let store = UnifiedTokenStore::new(TokenStorageBackend::File).unwrap();
197 let _ = store.exists();
200 }
201}