use crate::error::Error;
use crate::p2p::DnsResolver;
use crate::path::{IpfsPath, PathRoot};
use crate::Ipfs;
mod dnslink;
#[derive(Clone, Debug)]
#[cfg_attr(not(feature = "experimental"), allow(dead_code))]
pub struct Ipns {
ipfs: Ipfs,
}
#[cfg(feature = "experimental")]
#[derive(Clone, Copy, Debug, Default)]
pub enum IpnsOption {
Local,
#[default]
DHT,
}
impl Ipns {
pub fn new(ipfs: Ipfs) -> Self {
Ipns { ipfs }
}
pub async fn resolve(&self, resolver: DnsResolver, path: &IpfsPath) -> Result<IpfsPath, Error> {
let path = path.to_owned();
match path.root() {
PathRoot::Ipld(_) => Ok(path),
#[cfg(feature = "experimental")]
PathRoot::Ipns(peer) => {
use std::str::FromStr;
use std::time::Duration;
use futures::StreamExt;
use libipld::Cid;
use libp2p::PeerId;
let hash: libipld::multihash::Multihash =
libipld::multihash::Multihash::from_bytes(&peer.to_bytes())?;
let cid = Cid::new_v1(0x72, hash);
let mb = format!(
"/ipns/{}",
cid.to_string_of_base(libipld::multibase::Base::Base36Lower)?
);
let repo = self.ipfs.repo();
let datastore = repo.data_store();
if let Ok(Some(data)) = datastore.get(mb.as_bytes()).await {
if let Ok(path) = rust_ipns::Record::decode(data).and_then(|record| {
record.verify(*peer)?;
let data = record.data()?;
IpfsPath::from_str(&String::from_utf8_lossy(data.value()))
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}) {
return Ok(path);
}
}
let stream = self.ipfs.dht_get(mb).await?;
let mut records = tokio::time::timeout(
Duration::from_secs(60 * 2),
stream
.filter_map(|record| async move {
let key = &record.key.as_ref()[6..];
let record = rust_ipns::Record::decode(&record.value).ok()?;
let peer_id = PeerId::from_bytes(key).ok()?;
record.verify(peer_id).ok()?;
Some(record)
})
.collect::<Vec<_>>(),
)
.await
.unwrap_or_default();
if records.is_empty() {
anyhow::bail!("No records found")
}
records.sort_by_key(|record| record.sequence());
let record = records.last().ok_or(anyhow::anyhow!("No records found"))?;
let data = record.data()?;
let path = String::from_utf8_lossy(data.value()).to_string();
IpfsPath::from_str(&path).map_err(anyhow::Error::from)
}
#[cfg(not(feature = "experimental"))]
PathRoot::Ipns(_) => Err(anyhow::anyhow!("unimplemented")),
PathRoot::Dns(domain) => Ok(dnslink::resolve(resolver, domain).await?),
}
}
#[cfg(feature = "experimental")]
pub async fn publish(
&self,
key: Option<&str>,
path: &IpfsPath,
option: Option<IpnsOption>,
) -> Result<IpfsPath, Error> {
use libipld::Cid;
use libp2p::kad::Quorum;
use std::str::FromStr;
let keypair = match key {
Some(key) => self.ipfs.keystore().get_keypair(key).await?,
None => self.ipfs.keypair()?.clone(),
};
let peer_id = keypair.public().to_peer_id();
let hash: libipld::multihash::Multihash =
libipld::multihash::Multihash::from_bytes(&peer_id.to_bytes())?;
let cid = Cid::new_v1(0x72, hash);
let mb = format!(
"/ipns/{}",
cid.to_string_of_base(libipld::multibase::Base::Base36Lower)?
);
let repo = self.ipfs.repo();
let datastore = repo.data_store();
let record_data = datastore.get(mb.as_bytes()).await?;
let mut seq = 0;
if let Some(record) = record_data.as_ref() {
let record = rust_ipns::Record::decode(record)?;
record.verify(peer_id)?;
let data = record.data()?;
let ipfs_path = IpfsPath::from_str(&String::from_utf8_lossy(data.value()))?;
if ipfs_path.eq(path) {
return IpfsPath::from_str(&mb);
}
seq = record.sequence() + 1;
}
let path_bytes = path.to_string();
let record = rust_ipns::Record::new(
&keypair,
path_bytes.as_bytes(),
chrono::Duration::hours(48),
seq,
60000,
)?;
let bytes = record.encode()?;
datastore.put(mb.as_bytes(), &bytes).await?;
match option.unwrap_or_default() {
IpnsOption::DHT => self.ipfs.dht_put(&mb, bytes, Quorum::One).await?,
IpnsOption::Local => {}
};
IpfsPath::from_str(&mb)
}
}