1use std::{
4 io,
5 path::Path,
6 time::{SystemTime, UNIX_EPOCH},
7};
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11use tokio::{
12 fs::{self, File},
13 io::{AsyncReadExt, AsyncWriteExt},
14};
15use tracing::{debug, trace};
16
17#[derive(Debug, Error)]
18pub enum CacheError {
19 #[error("Failed to read cache file: {0}")]
20 Read(io::Error),
21 #[error("Failed to write cache file: {0}")]
22 Write(io::Error),
23 #[error("Failed to create cache file directory: {0}")]
24 MkDir(io::Error),
25 #[error("Failed to parse cache file: {0}")]
26 Parse(serde_json::Error),
27}
28
29#[derive(Deserialize, Serialize, Debug)]
30pub struct CachedAccount {
31 pub email: String,
32 pub msa: ExpiringValue<crate::auth::AccessTokenResponse>,
34 pub xbl: ExpiringValue<crate::auth::XboxLiveAuth>,
36 pub mca: ExpiringValue<crate::auth::MinecraftAuthResponse>,
38 pub profile: crate::auth::ProfileResponse,
40}
41
42#[derive(Deserialize, Serialize, Debug)]
43pub struct ExpiringValue<T> {
44 pub expires_at: u64,
46 pub data: T,
47}
48
49impl<T> ExpiringValue<T> {
50 pub fn is_expired(&self) -> bool {
51 self.expires_at
52 < SystemTime::now()
53 .duration_since(UNIX_EPOCH)
54 .unwrap()
55 .as_secs()
56 }
57
58 pub fn get(&self) -> Option<&T> {
60 if self.is_expired() {
61 None
62 } else {
63 Some(&self.data)
64 }
65 }
66}
67
68impl<T: Clone> Clone for ExpiringValue<T> {
69 fn clone(&self) -> Self {
70 Self {
71 expires_at: self.expires_at,
72 data: self.data.clone(),
73 }
74 }
75}
76
77async fn get_entire_cache(cache_file: &Path) -> Result<Vec<CachedAccount>, CacheError> {
78 let mut cache: Vec<CachedAccount> = Vec::new();
79 if cache_file.exists() {
80 let mut cache_file = File::open(cache_file).await.map_err(CacheError::Read)?;
81 let mut contents = String::new();
83 cache_file
84 .read_to_string(&mut contents)
85 .await
86 .map_err(CacheError::Read)?;
87 cache = serde_json::from_str(&contents).map_err(CacheError::Parse)?;
88 }
89 Ok(cache)
90}
91async fn set_entire_cache(cache_file: &Path, cache: Vec<CachedAccount>) -> Result<(), CacheError> {
92 trace!("saving cache: {:?}", cache);
93
94 if !cache_file.exists() {
95 let cache_file_parent = cache_file
96 .parent()
97 .expect("Cache file is root directory and also doesn't exist.");
98 debug!(
99 "Making cache file parent directory at {}",
100 cache_file_parent.to_string_lossy()
101 );
102 fs::create_dir_all(cache_file_parent)
103 .await
104 .map_err(CacheError::MkDir)?;
105 }
106 let mut cache_file = File::create(cache_file).await.map_err(CacheError::Write)?;
107 let cache = serde_json::to_string_pretty(&cache).map_err(CacheError::Parse)?;
108 cache_file
109 .write_all(cache.as_bytes())
110 .await
111 .map_err(CacheError::Write)?;
112
113 Ok(())
114}
115
116pub async fn get_account_in_cache(cache_file: &Path, email: &str) -> Option<CachedAccount> {
122 let cache = get_entire_cache(cache_file).await.unwrap_or_default();
123 cache.into_iter().find(|account| account.email == email)
124}
125
126pub async fn set_account_in_cache(
127 cache_file: &Path,
128 email: &str,
129 account: CachedAccount,
130) -> Result<(), CacheError> {
131 let mut cache = get_entire_cache(cache_file).await.unwrap_or_default();
132 cache.retain(|account| account.email != email);
133 cache.push(account);
134 set_entire_cache(cache_file, cache).await
135}