use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::io::AsyncWriteExt;
use super::MinecraftSession;
use crate::SecretString;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CachedAccount {
pub version: u32,
pub client_id: String,
pub microsoft_refresh_token: Option<SecretString>,
pub minecraft: Option<MinecraftSession>,
}
impl CachedAccount {
pub const VERSION: u32 = 1;
}
#[derive(Debug, Error)]
pub enum CacheError {
#[error("I/O error on {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("invalid cache file: {0}")]
Format(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenCache {
path: PathBuf,
}
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
impl TokenCache {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn path(&self) -> &Path {
&self.path
}
pub async fn load(&self) -> Result<Option<CachedAccount>, CacheError> {
let bytes = match tokio::fs::read(&self.path).await {
Ok(bytes) => bytes,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(source) => {
return Err(CacheError::Io {
path: self.path.clone(),
source,
});
}
};
let account: CachedAccount =
serde_json::from_slice(&bytes).map_err(|e| CacheError::Format(e.to_string()))?;
if account.version != CachedAccount::VERSION {
return Err(CacheError::Format(format!(
"unsupported version {}",
account.version
)));
}
Ok(Some(account))
}
pub async fn save(&self, account: &CachedAccount) -> Result<(), CacheError> {
let json =
serde_json::to_vec_pretty(account).map_err(|e| CacheError::Format(e.to_string()))?;
let io_err = |path: &Path| {
let path = path.to_path_buf();
move |source| CacheError::Io { path, source }
};
let dir = match self.path.parent() {
Some(dir) if !dir.as_os_str().is_empty() => dir.to_path_buf(),
_ => PathBuf::from("."),
};
let mut builder = tokio::fs::DirBuilder::new();
builder.recursive(true);
#[cfg(unix)]
builder.mode(0o700);
builder.create(&dir).await.map_err(io_err(&dir))?;
let file_name = self
.path
.file_name()
.ok_or_else(|| CacheError::Format("cache path has no file name".into()))?;
let mut tmp_name = file_name.to_os_string();
tmp_name.push(format!(
".tmp-{}-{}",
std::process::id(),
TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
));
let tmp = dir.join(tmp_name);
let result = async {
let mut options = tokio::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
options.mode(0o600);
let mut file = options.open(&tmp).await?;
file.write_all(&json).await?;
file.sync_all().await?;
drop(file);
tokio::fs::rename(&tmp, &self.path).await
}
.await;
if let Err(source) = result {
let _ = tokio::fs::remove_file(&tmp).await;
return Err(io_err(&self.path)(source));
}
Ok(())
}
pub async fn clear(&self) -> Result<(), CacheError> {
match tokio::fs::remove_file(&self.path).await {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(CacheError::Io {
path: self.path.clone(),
source,
}),
}
}
}