radicle-heartwood-lfs/crates/radicle-cli/src/commands/lfs/fetch.rs

62 lines
2.3 KiB
Rust

//! `rad lfs fetch` — plumbing command invoked by the Git LFS custom
//! transfer agent (`rad-lfs-transfer`) to download one LFS object's
//! content from the local IPFS (Kubo) node, decrypting it first if the
//! repository is private.
use std::path::PathBuf;
use anyhow::{Context as _, anyhow};
use crate::ipfs;
use crate::lfs_crypto;
use crate::terminal as term;
pub fn run(oid: String, size: i64, out: PathBuf, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let (repo, rid) = radicle::rad::cwd()
.context("`rad lfs fetch` must be run inside a Radicle repository working copy")?;
let pointer_text =
format!("version https://git-lfs.github.com/spec/v1\noid sha256:{oid}\nsize {size}\n");
let blob_oid = repo
.blob(pointer_text.as_bytes())
.context("failed to write LFS pointer blob")?;
let candidates = lfs_crypto::find_envelopes(&repo, blob_oid)
.with_context(|| format!("failed to read LFS notes for oid {oid}"))?;
if candidates.is_empty() {
anyhow::bail!(
"no CID recorded for oid {oid}; the peer that has this object needs to push its \
{} ref",
lfs_crypto::NOTES_REF
);
}
ipfs::check_daemon()?;
// Almost always exactly one candidate. More than one only happens if
// two peers independently encrypted byte-identical content (same
// oid/size) producing different ciphertext each time -- try each
// until one actually decrypts for us.
let mut last_err = None;
for envelope in candidates {
let result = match &envelope.enc {
None => ipfs::cat(&envelope.cid),
Some(enc) => ipfs::cat(&envelope.cid).and_then(|ciphertext| {
let signer = lfs_crypto::load_signer(&profile)?;
lfs_crypto::decrypt_with(&ciphertext, enc, &signer, profile.did(), &rid, &oid)
}),
};
match result {
Ok(bytes) => {
std::fs::write(&out, bytes)
.with_context(|| format!("failed to write `{}`", out.display()))?;
return Ok(());
}
Err(err) => last_err = Some(err),
}
}
Err(last_err.unwrap_or_else(|| anyhow!("no CID recorded for oid {oid}")))
}