use anyhow::{bail, Result};
use clap::Args;
use clap_complete::engine::{ArgValueCompleter, PathCompleter};
use std::collections::HashMap;
use std::io::Read;
use std::path::PathBuf;
use yb_core::orchestrator::{self, Compression, Encryption, StoreOptions};
use yb_core::store::{constants::MAX_NAME_LEN, Store};
use yb_core::{errors::YbError, Context, SecretOp, SlotKeyCheck};
#[derive(Args, Debug)]
pub struct StoreArgs {
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
pub files: Vec<PathBuf>,
#[arg(short = 'n', long = "name")]
pub name: Option<String>,
#[arg(
short = 'e',
long = "encrypted",
default_value_t = true,
overrides_with = "unencrypted"
)]
pub encrypted: bool,
#[arg(short = 'u', long = "unencrypted")]
pub unencrypted: bool,
#[arg(long = "no-compress")]
pub no_compress: bool,
}
pub fn run(ctx: &Context, args: &StoreArgs) -> Result<()> {
ctx.enforce_default_policy(SecretOp::Store)?;
let encrypted = !args.unencrypted;
let compression = match args.no_compress {
true => Compression::None,
false => Compression::Auto,
};
let entries: Vec<(String, Vec<u8>)> = if args.files.is_empty() {
let name = args
.name
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--name is required when reading from stdin"))?
.to_owned();
let mut buf = Vec::new();
std::io::stdin().read_to_end(&mut buf)?;
vec![(name, buf)]
} else {
if args.name.is_some() && args.files.len() > 1 {
bail!("--name can only be used with a single file");
}
let mut entries = Vec::with_capacity(args.files.len());
for path in &args.files {
let name = if let Some(ref n) = args.name {
n.clone()
} else {
path.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_owned())
.ok_or_else(|| anyhow::anyhow!("cannot derive blob name from {:?}", path))?
};
let payload = std::fs::read(path)?;
entries.push((name, payload));
}
let mut seen: HashMap<&str, &PathBuf> = HashMap::new();
for (i, (name, _)) in entries.iter().enumerate() {
if let Some(prev) = seen.get(name.as_str()) {
bail!(
"duplicate blob name '{}' from '{}' and '{}'",
name,
prev.display(),
args.files[i].display()
);
}
seen.insert(name.as_str(), &args.files[i]);
}
entries
};
let mut store = Store::from_device(&ctx.reader, ctx.piv.as_ref())?;
store.sanitize();
let peer_pk = if encrypted {
Some(ctx.get_public_key(store.store_key_slot)?)
} else {
None
};
let mgmt_key = ctx.management_key_for_write()?;
let total_chunks: usize = entries
.iter()
.map(|(name, payload)| {
let enc_len = if encrypted {
payload.len() + yb_core::crypto::GCM_OVERHEAD
} else {
payload.len()
};
let name_len = name.len().min(MAX_NAME_LEN);
yb_core::orchestrator::chunks_needed(enc_len, name_len, store.object_size)
})
.sum();
let freed_chunks: usize = entries
.iter()
.filter_map(|(name, _)| store.find_head(name))
.map(|head| store.chunk_chain(head.index()).len())
.sum();
if store.free_count() + freed_chunks < total_chunks {
if entries.len() == 1 {
bail!(
"store is full โ remove some blobs first (need {} slot(s), {} free)",
total_chunks,
store.free_count()
);
} else {
bail!(
"store is full โ need {} slots for {} files, only {} free",
total_chunks,
entries.len(),
store.free_count()
);
}
}
let slot = store.store_key_slot;
if ctx.check_slot_key(slot)? == SlotKeyCheck::Mismatch {
return Err(YbError::new(format!(
"the key in slot 0x{slot:02x} does not match its certificate"
))
.why("new blobs would be undecryptable; nothing was stored")
.fix("`yb format --generate` (this erases the store)")
.into());
}
let pin = ctx.require_pin()?;
for (name, payload) in &entries {
let ok = orchestrator::store_blob(
&mut store,
ctx.piv.as_ref(),
name,
payload,
StoreOptions {
encryption: match peer_pk.as_ref() {
Some(pk) => Encryption::Encrypted(pk),
None => Encryption::None,
},
compression,
},
&mgmt_key,
pin.as_deref(),
)?;
if !ok {
bail!(
"store is full while writing '{}' โ this should not happen",
name
);
}
if !ctx.quiet {
eprintln!(
"Stored '{}' ({} bytes{})",
name,
payload.len(),
if encrypted { ", encrypted" } else { "" }
);
}
}
ctx.complete_pending_repairs();
Ok(())
}