From d75ab03da70a0d97d7a083469ae2df1ec05259f8 Mon Sep 17 00:00:00 2001 From: Jonas Mueller Date: Sun, 26 Apr 2026 21:28:29 +0200 Subject: [PATCH] Allow --json option for list --- Cargo.lock | 1 + crates/vykar-cli/Cargo.toml | 1 + crates/vykar-cli/src/cli.rs | 4 ++ crates/vykar-cli/src/cmd/list.rs | 57 ++++++++++++++++++++++- crates/vykar-cli/src/dispatch.rs | 4 +- crates/vykar-cli/tests/cli_integration.rs | 26 +++++++++++ 6 files changed, 89 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d66e975..93e5c263 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8176,6 +8176,7 @@ dependencies = [ "libc", "rand 0.9.4", "serde", + "serde_json", "serde_yaml", "tempfile", "tracing", diff --git a/crates/vykar-cli/Cargo.toml b/crates/vykar-cli/Cargo.toml index 63c28e9e..6123a11a 100644 --- a/crates/vykar-cli/Cargo.toml +++ b/crates/vykar-cli/Cargo.toml @@ -19,6 +19,7 @@ vykar-storage.workspace = true clap.workspace = true serde.workspace = true serde_yaml.workspace = true +serde_json.workspace = true tracing.workspace = true tracing-subscriber.workspace = true comfy-table = "7" diff --git a/crates/vykar-cli/src/cli.rs b/crates/vykar-cli/src/cli.rs index eb0d0991..d9bbf907 100644 --- a/crates/vykar-cli/src/cli.rs +++ b/crates/vykar-cli/src/cli.rs @@ -102,6 +102,10 @@ pub(crate) enum Commands { /// Show only the N most recent snapshots #[arg(long)] last: Option, + + /// Output data as JSON + #[arg(long)] + json: bool, }, /// Inspect snapshot contents and metadata diff --git a/crates/vykar-cli/src/cmd/list.rs b/crates/vykar-cli/src/cmd/list.rs index 43874106..6fb319d6 100644 --- a/crates/vykar-cli/src/cmd/list.rs +++ b/crates/vykar-cli/src/cmd/list.rs @@ -8,10 +8,49 @@ use crate::format::{format_bytes, format_count}; use crate::passphrase::with_repo_passphrase; use crate::table::CliTableTheme; +// use serde_json::Value; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use vykar_types::snapshot_id::SnapshotId; +use vykar_core::repo::manifest::SnapshotEntry; +use vykar_core::snapshot::SnapshotStats; + +#[derive(Serialize)] +struct SnapshotView { + pub name: String, + pub id: SnapshotId, + pub time: DateTime, + pub label: String, + pub source_paths: Vec, + pub hostname: String, + pub nfiles: u64, + pub original_size: u64, + pub compressed_size: u64, + pub deduplicated_size: u64, +} + +impl SnapshotView { + fn from_entry_stats_tuple(entry: SnapshotEntry, stats: SnapshotStats) -> SnapshotView { + Self { + name: entry.name, + id: entry.id, + time: entry.time, + label: entry.label, + source_paths: entry.source_paths, + hostname: entry.hostname, + nfiles: stats.nfiles, + original_size: stats.original_size, + compressed_size: stats.compressed_size, + deduplicated_size: stats.deduplicated_size, + } + } +} + pub(crate) fn run_list( config: &VykarConfig, label: Option<&str>, source_filter: &[String], + json: &bool, last: Option, ) -> Result<(), Box> { let mut snapshots = with_repo_passphrase(config, label, |passphrase| { @@ -31,8 +70,13 @@ pub(crate) fn run_list( snapshots.drain(..len - n); } } + if snapshots.is_empty() { - println!("No snapshots found."); + if *json { + println!("[]"); + } else { + println!("No snapshots found."); + } return Ok(()); } @@ -106,7 +150,16 @@ pub(crate) fn run_list( Cell::new(size_col), ]); } - println!("{table}"); + + if *json { + let snapshot_views: Vec = snapshots + .into_iter() + .map(|(entry, stats)| SnapshotView::from_entry_stats_tuple(entry, stats.unwrap())) + .collect(); + println!("{}", serde_json::to_string_pretty(&snapshot_views)?); + } else { + println!("{table}"); + } Ok(()) } diff --git a/crates/vykar-cli/src/dispatch.rs b/crates/vykar-cli/src/dispatch.rs index 1ca9a6a9..22363412 100644 --- a/crates/vykar-cli/src/dispatch.rs +++ b/crates/vykar-cli/src/dispatch.rs @@ -236,8 +236,8 @@ pub(crate) fn dispatch_command( shutdown, verbose, ), - Commands::List { source, last, .. } => { - cmd::list::run_list(cfg, label, source, *last).map(|()| false) + Commands::List { source, last, json, .. } => { + cmd::list::run_list(cfg, label, source, json, *last).map(|()| false) } Commands::Snapshot { command, .. } => { cmd::snapshot::run_snapshot_command(command, cfg, label, shutdown).map(|()| false) diff --git a/crates/vykar-cli/tests/cli_integration.rs b/crates/vykar-cli/tests/cli_integration.rs index d688d1bb..fb6a91c7 100644 --- a/crates/vykar-cli/tests/cli_integration.rs +++ b/crates/vykar-cli/tests/cli_integration.rs @@ -553,6 +553,32 @@ fn cli_snapshot_list_latest_alias() { ); } +#[test] +fn cli_snapshot_list_json() { + let fx = CliFixture::new(); + write_plain_config(&fx.config_path, &fx.repo_dir); + + let cfg = fx.config_path.to_string_lossy().to_string(); + let source = fx.source_a.to_string_lossy().to_string(); + + fx.run_ok(&["--config", &cfg, "init"]); + + // Snapshot 1: only old.txt + std::fs::write(fx.source_a.join("old.txt"), b"old\n").unwrap(); + fx.run_ok(&["--config", &cfg, "backup", &source]); + + let out = fx.run_ok(&["--config", &cfg, "list", "--json"]); + let out_json: serde_json::Value = serde_json::from_str(&out).unwrap(); + assert!( + out_json[0]["nfiles"] == 1 && + out_json[0]["source_paths"][0].to_string().contains("source-a") && + out_json[0]["original_size"] == 4 && + out_json[0]["compressed_size"] == 11 && + out_json[0]["deduplicated_size"] == 11, + "expected list --json to show valid info, got:\n{out}" + ); +} + #[test] fn cli_snapshot_info_latest_alias() { let fx = CliFixture::new();