use crate::env::version;
use crate::locator::PavexLocator;
use anyhow::Context;
use pavex_cli_flock::{FileLock, Filesystem};
use redact::Secret;
use std::io::{Read, Write};
pub struct State {
filesystem: Filesystem,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Default)]
struct StateInner {
#[serde(skip_serializing_if = "Option::is_none")]
toolchain: Option<semver::Version>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "serialize_activation_key")]
activation_key: Option<Secret<String>>,
}
fn serialize_activation_key<S>(
activation_key: &Option<Secret<String>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match activation_key {
Some(activation_key) => serializer.serialize_some(activation_key.expose_secret()),
None => serializer.serialize_none(),
}
}
impl State {
const STATE_FILENAME: &'static str = "state.toml";
pub fn new(locator: &PavexLocator) -> Self {
let filesystem = Filesystem::new(locator.root_dir().to_owned());
Self { filesystem }
}
pub fn get_current_toolchain(&self) -> Result<semver::Version, StateReadError> {
let (_, current_state) = self.immutable_read()?;
let toolchain = current_state.and_then(|s| s.toolchain);
match toolchain {
Some(toolchain) => Ok(toolchain),
None => {
Ok(version())
}
}
}
pub fn get_activation_key(&self) -> Result<Option<Secret<String>>, StateReadError> {
let (_, current_state) = self.immutable_read()?;
Ok(current_state.and_then(|s| s.activation_key))
}
pub fn set_activation_key(&self, activation_key: Secret<String>) -> Result<(), anyhow::Error> {
let (mut locked_file, state) = self.read_for_update()?;
let mut state = state.unwrap_or_default();
state.activation_key = Some(activation_key);
let state = toml::to_string_pretty(&state)
.context("Failed to serialize Pavex's updated state in TOML format.")?;
locked_file.write_all(state.as_bytes()).context(format!(
"Failed to write Pavex's updated state to {}.",
Self::STATE_FILENAME
))?;
Ok(())
}
pub fn set_current_toolchain(&self, toolchain: semver::Version) -> Result<(), anyhow::Error> {
let (mut locked_file, state) = self.read_for_update()?;
let mut state = state.unwrap_or_default();
if state.toolchain.as_ref() == Some(&toolchain) {
return Ok(());
} else {
state.toolchain = Some(toolchain);
}
let state = toml::to_string_pretty(&state)
.context("Failed to serialize Pavex's updated state in TOML format.")?;
locked_file.write_all(state.as_bytes()).context(format!(
"Failed to write Pavex's updated state to {}.",
Self::STATE_FILENAME
))?;
Ok(())
}
fn read_for_update(&self) -> Result<(FileLock, Option<StateInner>), StateReadError> {
let locked_file = self
.filesystem
.open_rw_exclusive_create(Self::STATE_FILENAME, "Pavex's state file")
.map_err(AcquireLockError)?;
self._read(locked_file)
}
fn immutable_read(&self) -> Result<(FileLock, Option<StateInner>), StateReadError> {
let locked_file = self
.filesystem
.open_ro_shared_create(Self::STATE_FILENAME, "Pavex's state file")
.map_err(AcquireLockError)?;
self._read(locked_file)
}
fn _read(
&self,
mut locked_file: FileLock,
) -> Result<(FileLock, Option<StateInner>), StateReadError> {
let mut contents = String::new();
locked_file
.read_to_string(&mut contents)
.map_err(|e| StateReadError::ReadError(e, Self::STATE_FILENAME))?;
if contents.is_empty() {
Ok((locked_file, None))
} else {
let contents = toml::from_str(&contents)
.map_err(|e| StateReadError::CannotParse(e, Self::STATE_FILENAME))?;
Ok((locked_file, Some(contents)))
}
}
}
#[derive(thiserror::Error, Debug)]
#[error("Failed to acquire a lock on Pavex's state file.")]
pub struct AcquireLockError(#[from] anyhow::Error);
#[derive(thiserror::Error, Debug)]
pub enum StateReadError {
#[error("Failed to parse Pavex's state file, {1}: {0}")]
CannotParse(#[source] toml::de::Error, &'static str),
#[error("Failed to read Pavex's state file, {1}: {0}")]
ReadError(#[source] std::io::Error, &'static str),
#[error(transparent)]
AcquireLock(#[from] AcquireLockError),
}