You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
distant/src/cli/cache.rs

110 lines
3.4 KiB
Rust

use crate::paths::user::CACHE_FILE_PATH;
use anyhow::Context;
use distant_core::ConnectionId;
use serde::{Deserialize, Serialize};
use std::{
io,
path::{Path, PathBuf},
};
mod id;
pub use id::CacheId;
/// Represents a disk-backed cache of data
#[derive(Clone, Debug)]
pub struct Cache {
file: CacheFile,
pub data: CacheData,
}
impl Cache {
pub fn new(custom_path: impl Into<Option<PathBuf>>) -> Self {
Self {
file: CacheFile::new(custom_path),
data: CacheData::default(),
}
}
/// Loads the cache from the specified file path, or default user-local cache path,
/// constructing data from the default cache if not found
pub async fn read_from_disk_or_default(
custom_path: impl Into<Option<PathBuf>>,
) -> anyhow::Result<Self> {
let file = CacheFile::new(custom_path);
let data = file.read_or_default().await?;
Ok(Self { file, data })
}
/// Writes the cache back to disk
pub async fn write_to_disk(&self) -> anyhow::Result<()> {
self.file.write(&self.data).await
}
}
/// Points to a cache file to support reading, writing, and editing the data
#[derive(Clone, Debug)]
pub struct CacheFile {
path: PathBuf,
}
impl CacheFile {
/// Creates a new [`CacheFile`] from the given path, defaulting to a user-local cache path
/// if none is provided
pub fn new(custom_path: impl Into<Option<PathBuf>>) -> Self {
Self {
path: custom_path
.into()
.unwrap_or_else(|| CACHE_FILE_PATH.to_path_buf()),
}
}
async fn read_or_default(&self) -> anyhow::Result<CacheData> {
CacheData::read_or_default(self.path.as_path())
.await
.with_context(|| format!("Failed to read cache from {:?}", self.path.as_path()))
}
async fn write(&self, data: &CacheData) -> anyhow::Result<()> {
data.write(self.path.as_path())
.await
.with_context(|| format!("Failed to write cache to {:?}", self.path.as_path()))
}
}
/// Provides quick access to cli-specific cache for a user
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CacheData {
/// Connection id of selected connection (or 0 if nothing selected)
pub selected: CacheId<ConnectionId>,
}
impl CacheData {
/// Reads the cache data from disk
async fn read(path: impl AsRef<Path>) -> io::Result<Self> {
let bytes = tokio::fs::read(path).await?;
toml_edit::de::from_slice(&bytes).map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))
}
/// Reads the cache data if the file exists, otherwise returning a default cache instance
async fn read_or_default(path: impl AsRef<Path>) -> io::Result<Self> {
match Self::read(path).await {
Ok(cache) => Ok(cache),
Err(x) if x.kind() == io::ErrorKind::NotFound => Ok(Self::default()),
Err(x) => Err(x),
}
}
/// Writes the cache data to disk
async fn write(&self, path: impl AsRef<Path>) -> io::Result<()> {
let bytes = toml_edit::ser::to_vec(self)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
// Ensure the parent directory of the cache exists
if let Some(parent) = path.as_ref().parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(path, bytes).await
}
}