radicle-lfs-transfer/src/kubo.rs

104 lines
3.2 KiB
Rust

use anyhow::{Context, bail};
use serde::Deserialize;
#[derive(Clone)]
pub struct Kubo {
client: reqwest::Client,
api_url: String,
}
#[derive(Deserialize)]
struct AddResponse {
#[serde(rename = "Hash")]
hash: String,
}
impl Kubo {
pub fn new(api_url: String) -> Self {
Self {
client: reqwest::Client::new(),
api_url,
}
}
/// Adds bytes to IPFS using Kubo's normal chunked UnixFS add (default
/// params — no raw-leaves / single-block tricks) and returns the CID.
pub async fn add(&self, bytes: Vec<u8>) -> anyhow::Result<String> {
let part = reqwest::multipart::Part::bytes(bytes).file_name("blob");
let form = reqwest::multipart::Form::new().part("file", part);
let resp = self
.client
.post(format!("{}/api/v0/add", self.api_url))
.multipart(form)
.send()
.await
.context("failed to reach Kubo daemon for /api/v0/add")?;
if !resp.status().is_success() {
bail!("kubo add failed: HTTP {}", resp.status());
}
let body = resp.text().await?;
let line = body
.lines()
.find(|l| !l.trim().is_empty())
.context("empty response from kubo add")?;
let parsed: AddResponse = serde_json::from_str(line)
.with_context(|| format!("failed to parse kubo add response: {line}"))?;
Ok(parsed.hash)
}
/// Explicitly (recursively) pins a CID, decoupled from add-time pinning.
pub async fn pin_add(&self, cid: &str) -> anyhow::Result<()> {
let resp = self
.client
.post(format!("{}/api/v0/pin/add", self.api_url))
.query(&[("arg", cid), ("recursive", "true")])
.send()
.await
.context("failed to reach Kubo daemon for /api/v0/pin/add")?;
if !resp.status().is_success() {
bail!("kubo pin/add failed for {cid}: HTTP {}", resp.status());
}
Ok(())
}
/// Streams the full reassembled file content for a CID via /api/v0/cat.
pub async fn cat(&self, cid: &str) -> anyhow::Result<reqwest::Response> {
let resp = self
.client
.post(format!("{}/api/v0/cat", self.api_url))
.query(&[("arg", cid)])
.send()
.await
.context("failed to reach Kubo daemon for /api/v0/cat")?;
if !resp.status().is_success() {
bail!("kubo cat failed for {cid}: HTTP {}", resp.status());
}
Ok(resp)
}
/// Verifies the local Kubo daemon is actually reachable, for a clear
/// upfront error instead of a confusing failure mid-transfer.
pub async fn healthcheck(&self) -> anyhow::Result<()> {
let resp = self
.client
.post(format!("{}/api/v0/id", self.api_url))
.send()
.await
.with_context(|| {
format!(
"no IPFS (Kubo) daemon reachable at {} — start one with `ipfs daemon`",
self.api_url
)
})?;
if !resp.status().is_success() {
bail!("kubo daemon at {} returned HTTP {}", self.api_url, resp.status());
}
Ok(())
}
}