Skip to content
4 changes: 4 additions & 0 deletions crates/cli/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use clap::Subcommand;

mod inspect;
mod purge;
mod stamp;
mod upgrade;
mod verify;
Expand All @@ -15,6 +16,8 @@ pub enum Commands {
Stamp(stamp::Stamp),
/// Upgrade timestamp
Upgrade(upgrade::Upgrade),
/// Purge stale pending attestations
Purge(purge::Purge),
}

impl Commands {
Expand All @@ -24,6 +27,7 @@ impl Commands {
Commands::Verify(cmd) => cmd.run().await,
Commands::Stamp(cmd) => cmd.run().await,
Commands::Upgrade(cmd) => cmd.run().await,
Commands::Purge(cmd) => cmd.run().await,
}
}
}
114 changes: 114 additions & 0 deletions crates/cli/src/commands/purge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use clap::Args;
use std::{collections::HashSet, path::PathBuf};
use tracing::{error, info, warn};
use uts_core::codec::{Decode, Encode, VersionedProof, v1::DetachedTimestamp};
use uts_sdk::Sdk;

#[derive(Debug, Args)]
pub struct Purge {
/// Files to purge pending attestations from. May be specified multiple times.
#[arg(value_name = "FILE", num_args = 1..)]
files: Vec<PathBuf>,
/// Skip the interactive confirmation prompt and purge all pending attestations.
#[arg(short = 'y', long = "yes", default_value_t = false)]
yes: bool,
}

impl Purge {
pub async fn run(self) -> eyre::Result<()> {
for path in &self.files {
if let Err(e) = self.purge_one(path).await {
error!("[{}] failed to purge: {e}", path.display());
}
}
Ok(())
}

async fn purge_one(&self, path: &PathBuf) -> eyre::Result<()> {
let file = tokio::fs::read(path).await?;
let mut proof = VersionedProof::<DetachedTimestamp>::decode(&mut &*file)?;

let pending = Sdk::list_pending(&proof);
if pending.is_empty() {
info!(
"[{}] no pending attestations found, skipping",
path.display()
);
return Ok(());
}

info!(
"[{}] found {} pending attestation(s):",
path.display(),
pending.len()
);
for (i, uri) in pending.iter().enumerate() {
info!(" [{}] {uri}", i + 1);
}

let uris_to_purge = if self.yes {
// Purge all when --yes flag is used
None
} else {
// Interactive selection
print!("Enter numbers to purge (comma-separated), 'all', or 'none' to skip: ");
use std::io::Write;
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let input = input.trim();

if input.eq_ignore_ascii_case("none") || input.is_empty() {
info!("[{}] skipped", path.display());
return Ok(());
}

if input.eq_ignore_ascii_case("all") {
None
} else {
let mut selected = HashSet::new();
for part in input.split(',') {
let part = part.trim();
match part.parse::<usize>() {
Ok(n) if n >= 1 && n <= pending.len() => {
selected.insert(pending[n - 1].clone());
}
_ => {
warn!("ignoring invalid selection: {part}");
}
}
}
if selected.is_empty() {
info!("[{}] no valid selections, skipping", path.display());
return Ok(());
}
Some(selected)
}
};

let result = Sdk::purge_pending_by_uris(&mut proof, uris_to_purge.as_ref());

if result.purged.is_empty() {
info!("[{}] nothing to purge", path.display());
return Ok(());
}

if !result.has_remaining {
warn!(
"[{}] purging would leave no attestations in the file, skipping",
path.display()
);
return Ok(());
}

let mut buf = Vec::new();
proof.encode(&mut buf)?;
tokio::fs::write(path, buf).await?;
info!(
"[{}] purged {} pending attestation(s)",
path.display(),
result.purged.len()
);
Ok(())
}
}
Loading
Loading