pavex_cli 0.2.10

The companion CLI for Pavex, a Rust framework to build API services and web applications.
Documentation
use crate::locator::AuthLocator;
use anyhow::Context;
use redact::Secret;
use std::io::Write;
use tokio::fs;
use tracing::instrument;

/// A file-based cache to hold the latest CLI token obtained by Pavex's API.
pub struct CliTokenDiskCache(AuthLocator);

#[derive(serde::Serialize, serde::Deserialize)]
struct CachedToken {
    #[serde(serialize_with = "redact::expose_secret")]
    jwt: Option<Secret<String>>,
}

impl CliTokenDiskCache {
    pub fn new(locator: AuthLocator) -> Self {
        Self(locator)
    }

    pub async fn get_token(&self) -> Result<Option<Secret<String>>, anyhow::Error> {
        self.get_cache().await.map(|data| data.and_then(|d| d.jwt))
    }

    async fn get_cache(&self) -> Result<Option<CachedToken>, anyhow::Error> {
        let cache_path = self.0.token_cache();
        let data = match fs::read(&cache_path).await {
            Ok(data) => data,
            Err(e) => {
                return if e.kind() == std::io::ErrorKind::NotFound {
                    Ok(None)
                } else {
                    Err(e.into())
                };
            }
        };
        let data = std::str::from_utf8(&data)
            .with_context(|| format!("`{}` contains non UTF-8 data", cache_path.display()))?;
        let data = toml::from_str(data).context("Failed to deserialize CLI token cache")?;
        Ok(data)
    }

    #[instrument(skip_all, "Updating the CLI token cache")]
    pub async fn upsert_token(&self, new_token: Secret<String>) -> Result<(), anyhow::Error> {
        // Strategy: first write the updated data to a temporary file.
        // Then rename that temporary file to the destination path.
        // On most filesystems, this should ensure that the update is atomic and
        // we won't end up with a corrupted file.
        // We locate the temp file in the same directory of the target file to minimise
        // the risk of them being on different storage devices.
        //
        // The update is racy: if multiple instances of Pavex CLI try to update the token,
        // the "last" wins.
        // But that's fine: any token is OK as long as it's fresh.
        let updated_data = match self.get_cache().await? {
            None => CachedToken {
                jwt: Some(new_token),
            },
            Some(mut data) => {
                data.jwt = Some(new_token);
                data
            }
        };

        let tmp_dir = self.0.tmp();
        fs::create_dir_all(&tmp_dir).await.with_context(|| {
            format!(
                "Failed to create `{}` to store temporary files.",
                tmp_dir.display()
            )
        })?;
        let mut tmp_file = tempfile::NamedTempFile::new_in(&tmp_dir)
            .context("Failed to create a temporary file")?;
        tmp_file.write_all(
            "# This file is autogenerated and managed by `pavex` CLI. Do NOT edit it.\n".as_bytes(),
        )?;
        let updated_data = toml::to_string(&updated_data)
            .context("Failed to serialize updated CLI token cache")?;
        tmp_file.write_all(updated_data.as_bytes())?;

        fs::rename(tmp_file.path(), &self.0.token_cache())
            .await
            .context("Failed to swap the existing CLI cache file with the updated one")
    }
}