syncweb_core/folder/
catalog.rs1use std::{str::FromStr, time::Duration};
2
3use iroh::{PublicKey, address_lookup::memory::MemoryLookup};
4use iroh_blobs::{Hash, ticket::BlobTicket};
5use iroh_gossip::{
6 TopicId,
7 api::{Event, GossipSender, GossipTopic},
8};
9use n0_future::StreamExt;
10use semver::Version;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14use crate::{
15 error::{Result, SyncwebError},
16 node::gossip_service::GossipService,
17};
18
19const CATALOG_TOPIC_SEED: &[u8] = b"syncweb/public-package-catalog/v1";
20
21#[must_use]
23pub fn catalog_topic() -> TopicId {
24 TopicId::from_bytes(*blake3::hash(CATALOG_TOPIC_SEED).as_bytes())
25}
26
27#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
29#[non_exhaustive]
30pub struct PackageAnnouncement {
31 pub collection_id: Uuid,
32 pub name: String,
33 pub version: String,
34 pub sequence: u64,
35 pub manifest: Hash,
36 pub manifest_ticket: String,
37 pub publisher: String,
38}
39
40impl PackageAnnouncement {
41 pub fn new(
46 collection_id: Uuid,
47 name: impl Into<String>,
48 version: impl Into<String>,
49 sequence: u64,
50 manifest: Hash,
51 manifest_ticket: impl Into<String>,
52 publisher: PublicKey,
53 ) -> Result<Self> {
54 let announcement = Self {
55 collection_id,
56 name: name.into(),
57 version: version.into(),
58 sequence,
59 manifest,
60 manifest_ticket: manifest_ticket.into(),
61 publisher: publisher.to_string(),
62 };
63 announcement.validate()?;
64 Ok(announcement)
65 }
66
67 pub fn validate(&self) -> Result<()> {
72 if self.name.trim().is_empty() {
73 return Err(SyncwebError::InvalidConfig(
74 "package announcement name cannot be empty".to_owned(),
75 ));
76 }
77 if self.sequence == 0 {
78 return Err(SyncwebError::InvalidConfig(
79 "package announcement sequence must be greater than zero".to_owned(),
80 ));
81 }
82 Version::parse(&self.version)
83 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid package version: {error}")))?;
84 let ticket = BlobTicket::from_str(&self.manifest_ticket)
85 .map_err(|error| SyncwebError::InvalidTicket(error.to_string()))?;
86 if ticket.hash() != self.manifest {
87 return Err(SyncwebError::InvalidTicket(
88 "package manifest ticket does not match its manifest hash".to_owned(),
89 ));
90 }
91 self.publisher
92 .parse::<PublicKey>()
93 .map_err(|error| SyncwebError::InvalidIdentity(error.to_string()))?;
94 Ok(())
95 }
96
97 pub fn to_bytes(&self) -> Result<Vec<u8>> {
101 self.validate()?;
102 serde_json::to_vec(self)
103 .map_err(|error| SyncwebError::operation("failed to serialize package announcement", error))
104 }
105
106 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
110 let announcement: Self = serde_json::from_slice(bytes.as_ref())
111 .map_err(|error| SyncwebError::operation("failed to deserialize package announcement", error))?;
112 announcement.validate()?;
113 Ok(announcement)
114 }
115
116 pub fn ticket(&self) -> Result<BlobTicket> {
122 BlobTicket::from_str(&self.manifest_ticket).map_err(|error| SyncwebError::InvalidTicket(error.to_string()))
123 }
124
125 fn matches(&self, query: Option<&str>) -> bool {
126 query.is_none_or(|needle| {
127 self.name.contains(needle)
128 || self.collection_id.to_string().contains(needle)
129 || self.version.contains(needle)
130 })
131 }
132}
133
134#[derive(Clone)]
136pub struct PackageCatalog {
137 gossip: GossipService,
138}
139
140impl PackageCatalog {
141 #[must_use]
142 pub fn new(gossip: &GossipService) -> Self {
143 Self { gossip: gossip.clone() }
144 }
145
146 pub async fn subscribe(&self, bootstrap: Vec<PublicKey>) -> Result<GossipTopic> {
155 self.gossip.subscribe(catalog_topic(), bootstrap).await
156 }
157
158 pub async fn subscribe_and_join(&self, bootstrap: Vec<PublicKey>) -> Result<GossipTopic> {
166 self.gossip.subscribe_and_join(catalog_topic(), bootstrap).await
167 }
168
169 pub async fn announce(&self, sender: &GossipSender, announcement: &PackageAnnouncement) -> Result<()> {
175 self.gossip.publish(sender, announcement.to_bytes()?).await
176 }
177
178 pub async fn search(
187 &self,
188 topic: &mut GossipTopic,
189 query: Option<&str>,
190 timeout: Duration,
191 ) -> Result<Vec<PackageAnnouncement>> {
192 let mut announcements = Vec::new();
193 let receive = async {
194 while let Some(event) = topic.next().await {
195 if let Event::Received(message) =
196 event.map_err(|error| SyncwebError::operation("package catalog event failed", error))?
197 {
198 let announcement = PackageAnnouncement::from_bytes(message.content)?;
199 if announcement.matches(query)
200 && !announcements.iter().any(|item: &PackageAnnouncement| {
201 item.collection_id == announcement.collection_id
202 && item.version == announcement.version
203 && item.manifest == announcement.manifest
204 })
205 {
206 announcements.push(announcement);
207 }
208 }
209 }
210 Ok::<(), SyncwebError>(())
211 };
212 if let Ok(result) = tokio::time::timeout(timeout, receive).await {
213 result?;
214 }
215 Ok(announcements)
216 }
217
218 pub fn register_ticket_endpoint(lookup: &MemoryLookup, announcement: &PackageAnnouncement) -> Result<()> {
227 let ticket = announcement.ticket()?;
228 lookup.add_endpoint_info(ticket.addr().clone());
229 Ok(())
230 }
231}