use serde::Serialize;
use std::path::Path;
use void_core::cid::ToVoidCid;
use void_core::metadata::ShardMap;
use void_core::pipeline::{seal_workspace, SealOptions};
use void_core::shard::PaddingStrategy;
use crate::context::{open_repo, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug)]
pub struct SealArgs {
pub target_shard_size: Option<u64>,
pub max_shard_size: Option<u64>,
pub padding: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SealOutput {
pub metadata: String,
pub files: u64,
pub shards: u64,
pub stats: SealStats,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SealStats {
pub bytes_read: u64,
pub bytes_compressed: u64,
pub total_ms: u64,
}
fn parse_padding_strategy(s: &str) -> Result<PaddingStrategy, CliError> {
match s.to_lowercase().as_str() {
"none" => Ok(PaddingStrategy::None),
"power2" => Ok(PaddingStrategy::PowerOfTwo),
"buckets" => Ok(PaddingStrategy::Buckets),
other => other
.parse::<usize>()
.map(PaddingStrategy::Fixed)
.map_err(|_| {
CliError::invalid_args(format!(
"Invalid padding strategy '{}'. Use: none, power2, buckets, or a number",
other
))
}),
}
}
pub fn run(cwd: &Path, args: SealArgs, opts: &CliOptions) -> Result<(), CliError> {
run_command("seal", opts, |ctx| {
ctx.progress("Sealing workspace...");
let repo = open_repo(cwd)?;
let mut void_ctx = repo.context().clone();
if let Some(target) = args.target_shard_size {
void_ctx.seal.target_shard_size = target;
if args.max_shard_size.is_none() {
void_ctx.seal.max_shard_size = target.saturating_mul(3) / 2;
}
}
if let Some(max) = args.max_shard_size {
void_ctx.seal.max_shard_size = max;
}
if let Some(ref s) = args.padding {
void_ctx.seal.padding = parse_padding_strategy(s)?;
}
let seal_opts = SealOptions {
ctx: void_ctx,
shard_map: ShardMap::new(64),
content_key: None,
parent_content_key: None,
};
ctx.progress("Creating sealed snapshot...");
let result = seal_workspace(seal_opts).map_err(void_err_to_cli)?;
if !ctx.use_json() {
let metadata_cid_str = result.metadata_cid.to_cid_string();
ctx.info(format!("Sealed workspace: {}", metadata_cid_str));
ctx.info(format!(" Files: {}", result.stats.files_sealed));
ctx.info(format!(" Shards: {}", result.stats.shards_created));
ctx.info(format!(" Size: {} bytes", result.stats.bytes_read));
}
Ok(SealOutput {
metadata: result.metadata_cid.to_cid_string(),
files: result.stats.files_sealed as u64,
shards: result.stats.shards_created as u64,
stats: SealStats {
bytes_read: result.stats.bytes_read as u64,
bytes_compressed: result.stats.bytes_compressed as u64,
total_ms: result.stats.total_ms as u64,
},
})
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::CliOptions;
use std::fs;
use tempfile::tempdir;
use void_core::crypto;
fn default_opts() -> CliOptions {
CliOptions {
human: true,
..Default::default()
}
}
fn default_args() -> SealArgs {
SealArgs {
target_shard_size: None,
max_shard_size: None,
padding: None,
}
}
fn setup_test_repo() -> (tempfile::TempDir, std::path::PathBuf, tempfile::TempDir, crate::context::VoidHomeGuard) {
let dir = tempdir().unwrap();
let void_dir = dir.path().join(".void");
fs::create_dir_all(void_dir.join("objects")).unwrap();
let key = crypto::generate_key();
let home = tempdir().unwrap();
let guard = crate::context::setup_test_manifest(&void_dir, &key, home.path());
fs::write(void_dir.join("config.json"), "{}").unwrap();
fs::write(dir.path().join("test.txt"), "hello world").unwrap();
(dir, void_dir, home, guard)
}
#[test]
fn test_seal_creates_metadata() {
let (dir, _void_dir, _home, _guard) = setup_test_repo();
let result = run(dir.path(), default_args(), &default_opts());
assert!(result.is_ok());
}
#[test]
fn test_seal_not_initialized() {
let dir = tempdir().unwrap();
let result = run(dir.path(), default_args(), &default_opts());
assert!(result.is_err());
}
#[test]
fn test_parse_padding_strategy() {
assert!(matches!(
parse_padding_strategy("none"),
Ok(PaddingStrategy::None)
));
assert!(matches!(
parse_padding_strategy("NONE"),
Ok(PaddingStrategy::None)
));
assert!(matches!(
parse_padding_strategy("power2"),
Ok(PaddingStrategy::PowerOfTwo)
));
assert!(matches!(
parse_padding_strategy("buckets"),
Ok(PaddingStrategy::Buckets)
));
assert!(matches!(
parse_padding_strategy("4096"),
Ok(PaddingStrategy::Fixed(4096))
));
assert!(parse_padding_strategy("invalid").is_err());
}
}