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 #[serde(alias = "email")]
32 pub cache_key: String,
33 pub msa: ExpiringValue<crate::auth::AccessTokenResponse>,
35 pub xbl: ExpiringValue<crate::auth::XboxLiveAuth>,
37 pub mca: ExpiringValue<crate::auth::MinecraftAuthResponse>,
39 pub profile: crate::auth::ProfileResponse,
41}
42
43#[derive(Deserialize, Serialize, Debug)]
44pub struct ExpiringValue<T> {
45 pub expires_at: u64,
47 pub data: T,
48}
49
50impl<T> ExpiringValue<T> {
51 pub fn is_expired(&self) -> bool {
52 self.expires_at
53 < SystemTime::now()
54 .duration_since(UNIX_EPOCH)
55 .unwrap()
56 .as_secs()
57 }
58
59 pub fn get(&self) -> Option<&T> {
61 if self.is_expired() {
62 None
63 } else {
64 Some(&self.data)
65 }
66 }
67}
68
69impl<T: Clone> Clone for ExpiringValue<T> {
70 fn clone(&self) -> Self {
71 Self {
72 expires_at: self.expires_at,
73 data: self.data.clone(),
74 }
75 }
76}
77
78async fn get_entire_cache(cache_file: &Path) -> Result<Vec<CachedAccount>, CacheError> {
79 let mut cache: Vec<CachedAccount> = Vec::new();
80 if cache_file.exists() {
81 let mut cache_file = File::open(cache_file).await.map_err(CacheError::Read)?;
82 let mut contents = String::new();
84 cache_file
85 .read_to_string(&mut contents)
86 .await
87 .map_err(CacheError::Read)?;
88 cache = serde_json::from_str(&contents).map_err(CacheError::Parse)?;
89 }
90 Ok(cache)
91}
92async fn set_entire_cache(cache_file: &Path, cache: Vec<CachedAccount>) -> Result<(), CacheError> {
93 trace!("saving cache: {:?}", cache);
94
95 if !cache_file.exists() {
96 let cache_file_parent = cache_file
97 .parent()
98 .expect("Cache file is root directory and also doesn't exist.");
99 debug!(
100 "Making cache file parent directory at {}",
101 cache_file_parent.to_string_lossy()
102 );
103 fs::create_dir_all(cache_file_parent)
104 .await
105 .map_err(CacheError::MkDir)?;
106 }
107 let mut cache_file = File::create(cache_file).await.map_err(CacheError::Write)?;
108 let cache = serde_json::to_string_pretty(&cache).map_err(CacheError::Parse)?;
109 cache_file
110 .write_all(cache.as_bytes())
111 .await
112 .map_err(CacheError::Write)?;
113
114 Ok(())
115}
116
117pub async fn get_account_in_cache(cache_file: &Path, cache_key: &str) -> Option<CachedAccount> {
121 let cache = get_entire_cache(cache_file).await.unwrap_or_default();
122 cache
123 .into_iter()
124 .find(|account| account.cache_key == cache_key)
125}
126
127pub async fn set_account_in_cache(
128 cache_file: &Path,
129 cache_key: &str,
130 account: CachedAccount,
131) -> Result<(), CacheError> {
132 let mut cache = get_entire_cache(cache_file).await.unwrap_or_default();
133 cache.retain(|account| account.cache_key != cache_key);
134 cache.push(account);
135 set_entire_cache(cache_file, cache).await
136}