104 lines
3.2 KiB
Rust
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(())
|
|
}
|
|
}
|