use serde::Deserialize;
use std::io::Read;
use std::time::Duration;
use crate::ContentError;
use crate::config::IPFS_API_URL;
use crate::encode::{cid_to_digest_hex, digest_hex_to_cid, hex_to_bytes};
fn agent() -> ureq::Agent {
ureq::config::Config::builder()
.timeout_connect(Some(Duration::from_secs(5)))
.timeout_global(Some(Duration::from_secs(30)))
.build()
.new_agent()
}
#[derive(Deserialize)]
struct IpfsAddEntry {
#[serde(rename = "Hash")]
hash: String,
}
#[derive(Deserialize)]
pub struct IpfsPeerInfo {
#[serde(rename = "ID")]
pub peer_id: String,
#[serde(rename = "Addresses")]
pub addresses: Vec<String>,
}
pub fn add(bytes: &[u8], filename: &str) -> Result<String, ContentError> {
use ureq::unversioned::multipart::{Form, Part};
let form = Form::new().part("file", Part::bytes(bytes).file_name(filename));
let response = agent()
.post(&format!("{IPFS_API_URL}/api/v0/add"))
.query("pin", "true")
.query("quieter", "true")
.send(form)
.map_err(|e| ContentError::Ipfs(format!("failed to upload payload: {e}")))?;
let text = response
.into_body()
.read_to_string()
.map_err(|e| ContentError::Ipfs(format!("failed to read add response: {e}")))?;
let last_line = text
.lines()
.rfind(|l| !l.trim().is_empty())
.ok_or_else(|| ContentError::Ipfs("add returned an empty response".into()))?;
let entry: IpfsAddEntry = serde_json::from_str(last_line)
.map_err(|e| ContentError::Ipfs(format!("failed to decode add response: {e}")))?;
cid_to_digest_hex(&entry.hash)
}
pub fn cat(digest_hex: &str) -> Result<Vec<u8>, ContentError> {
let cid = digest_hex_to_cid(digest_hex)?;
cat_by_cid(&cid)
}
pub fn cat_by_cid(cid: &str) -> Result<Vec<u8>, ContentError> {
let response = agent()
.post(&format!("{IPFS_API_URL}/api/v0/cat"))
.query("arg", cid)
.send_empty()
.map_err(|e| ContentError::Ipfs(format!("failed to read {cid}: {e}")))?;
let mut buf = Vec::new();
response
.into_body()
.into_reader()
.read_to_end(&mut buf)
.map_err(|e| ContentError::Ipfs(format!("failed to read ipfs bytes for {cid}: {e}")))?;
Ok(buf)
}
pub fn id() -> Result<IpfsPeerInfo, ContentError> {
let response = agent()
.post(&format!("{IPFS_API_URL}/api/v0/id"))
.send_empty()
.map_err(|e| ContentError::Ipfs(format!("ipfs /id failed: {e}")))?;
let bytes = response
.into_body()
.read_to_string()
.map_err(|e| ContentError::Ipfs(format!("failed to read /id response: {e}")))?;
let info: IpfsPeerInfo = serde_json::from_str(&bytes)
.map_err(|e| ContentError::Ipfs(format!("failed to decode /id response: {e}")))?;
Ok(info)
}
pub fn digest_bytes(digest_hex: &str) -> Result<[u8; 32], ContentError> {
hex_to_bytes(digest_hex)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digest_bytes_validates() {
assert_eq!(
digest_bytes(&format!("0x{}", "ab".repeat(32))).unwrap(),
[0xab; 32]
);
assert!(digest_bytes("0x1234").is_err());
}
}