1pub mod catalog;
7pub mod denylist;
8pub mod links;
9pub use catalog::{Catalog, CatalogMetadata, CatalogRecord, CatalogService};
10pub use denylist::{Denied, DenyReason, Denylist, DenylistRule, DenylistService, FilterContext, FilterList};
11pub use links::{
12 CapabilityLink, ContentLink, ImmutableLink, Link, LinkResolution, LinkResolver, LinkStore, Mirror, MutableLink,
13 MutablePointer, NameLink, PrivateLink, ProviderFetch, ResolveOptions, ResolvedLink, SignedMutablePointer,
14 collect_files, fetch_from_mirrors, hash_source,
15};
16
17use std::{
18 collections::HashMap,
19 path::{Path, PathBuf},
20 str::FromStr,
21 sync::{Arc, Mutex},
22 time::{SystemTime, UNIX_EPOCH},
23};
24
25use iroh_blobs::Hash;
26use iroh_docs::{Entry, NamespaceId, engine::LiveEvent};
27use n0_future::StreamExt;
28use rusqlite::{Connection, OptionalExtension, params};
29use tokio::{sync::broadcast, task::JoinHandle};
30
31use crate::{
32 error::{Result, SyncwebError},
33 folder::SyncwebFolder,
34};
35
36pub const SCHEMA_VERSION: &str = "1";
38const EVENT_CAPACITY: usize = 256;
39
40#[derive(Clone, Debug, Eq, PartialEq)]
42#[non_exhaustive]
43pub struct IndexedEntry {
44 pub namespace_id: NamespaceId,
45 pub key: Vec<u8>,
46 pub hash: Hash,
47 pub size: u64,
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52#[non_exhaustive]
53pub enum CoreIndexingEvent {
54 ContentReady { hash: Hash },
55 PendingContentReady,
56 NeighborUp,
57 NeighborDown,
58 SyncFinished,
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
63#[non_exhaustive]
64pub enum IndexingEvent {
65 FolderEnabled {
66 namespace_id: NamespaceId,
67 },
68 FolderDisabled {
69 namespace_id: NamespaceId,
70 },
71 EntryIndexed(IndexedEntry),
72 Core {
73 namespace_id: NamespaceId,
74 event: CoreIndexingEvent,
75 },
76 Error {
77 namespace_id: NamespaceId,
78 message: String,
79 },
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84#[non_exhaustive]
85pub struct IndexingHandle {
86 namespace_id: NamespaceId,
87}
88
89impl IndexingHandle {
90 #[must_use]
91 pub const fn namespace_id(self) -> NamespaceId {
92 self.namespace_id
93 }
94}
95
96#[derive(Clone)]
98pub struct IndexingDatabase {
99 connection: Arc<Mutex<Connection>>,
100 path: Arc<PathBuf>,
101}
102
103impl std::fmt::Debug for IndexingDatabase {
104 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 formatter
106 .debug_struct("IndexingDatabase")
107 .field("path", &self.path)
108 .finish_non_exhaustive()
109 }
110}
111
112impl IndexingDatabase {
113 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
119 let database_path = path.as_ref().to_path_buf();
120 if database_path.as_os_str() != ":memory:"
121 && let Some(parent) = database_path.parent()
122 && !parent.as_os_str().is_empty()
123 {
124 std::fs::create_dir_all(parent)
125 .map_err(|error| SyncwebError::operation("failed to create indexing database directory", error))?;
126 }
127 let connection = Connection::open(&database_path)
128 .map_err(|error| SyncwebError::operation("failed to open indexing database", error))?;
129 initialize_connection(&connection)?;
130 Ok(Self {
131 connection: Arc::new(Mutex::new(connection)),
132 path: Arc::new(database_path),
133 })
134 }
135
136 pub fn in_memory() -> Result<Self> {
142 Self::open(":memory:")
143 }
144
145 #[must_use]
146 pub fn path(&self) -> &Path {
147 self.path.as_path()
148 }
149
150 pub fn schema_version(&self) -> Result<String> {
156 self.with_connection(|connection| {
157 connection
158 .query_row(
159 "SELECT value FROM index_metadata WHERE key = 'schema_version'",
160 [],
161 |row| row.get(0),
162 )
163 .map_err(|error| database_error("failed to read indexing schema version", error))
164 })
165 }
166
167 pub fn has_fts5(&self) -> Result<bool> {
173 self.has_table("indexed_entries_fts")
174 }
175
176 pub fn schema_objects(&self) -> Result<Vec<String>> {
182 self.with_connection(|connection| {
183 let mut statement = connection
184 .prepare("SELECT name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' ORDER BY name")
185 .map_err(|error| database_error("failed to prepare schema query", error))?;
186 let names = statement
187 .query_map([], |row| row.get::<_, String>(0))
188 .map_err(|error| database_error("failed to query schema objects", error))?
189 .collect::<std::result::Result<Vec<_>, _>>()
190 .map_err(|error| database_error("failed to read schema objects", error))?;
191 Ok(names)
192 })
193 }
194
195 pub fn has_table(&self, name: &str) -> Result<bool> {
201 self.with_connection(|connection| {
202 let exists = connection
203 .query_row(
204 "SELECT EXISTS(
205 SELECT 1 FROM sqlite_master
206 WHERE name = ?1 AND type IN ('table', 'view')
207 )",
208 [name],
209 |row| row.get(0),
210 )
211 .map_err(|error| database_error("failed to query schema object", error))?;
212 Ok(exists)
213 })
214 }
215
216 pub fn folder_count(&self) -> Result<usize> {
222 self.with_connection(|connection| {
223 let count = connection
224 .query_row("SELECT COUNT(*) FROM indexed_folders", [], |row| row.get::<_, i64>(0))
225 .map_err(|error| database_error("failed to count indexed folders", error))?;
226 usize::try_from(count).map_err(|error| database_error("indexed folder count is invalid", error))
227 })
228 }
229
230 pub fn entry_count(&self) -> Result<usize> {
236 self.with_connection(|connection| {
237 let count = connection
238 .query_row("SELECT COUNT(*) FROM indexed_entries", [], |row| row.get::<_, i64>(0))
239 .map_err(|error| database_error("failed to count indexed entries", error))?;
240 usize::try_from(count).map_err(|error| database_error("indexed entry count is invalid", error))
241 })
242 }
243
244 pub fn is_folder_enabled(&self, namespace_id: NamespaceId) -> Result<bool> {
250 self.with_connection(|connection| {
251 let enabled = connection
252 .query_row(
253 "SELECT EXISTS(
254 SELECT 1 FROM indexed_folders WHERE namespace_id = ?1
255 )",
256 [namespace_id.to_string()],
257 |row| row.get(0),
258 )
259 .map_err(|error| database_error("failed to query indexed folder", error))?;
260 Ok(enabled)
261 })
262 }
263
264 pub fn upsert_entry(
270 &self,
271 namespace_id: NamespaceId,
272 key: impl AsRef<[u8]>,
273 hash: Hash,
274 size: u64,
275 ) -> Result<IndexedEntry> {
276 let key_bytes = key.as_ref().to_vec();
277 let namespace = namespace_id.to_string();
278 let hash_bytes = hash.as_bytes().to_vec();
279 let size_value =
280 i64::try_from(size).map_err(|error| database_error("indexed entry size is too large", error))?;
281 let key_text = String::from_utf8_lossy(&key_bytes).into_owned();
282 let title = Path::new(&key_text)
283 .file_name()
284 .and_then(|name| name.to_str())
285 .unwrap_or(&key_text)
286 .to_owned();
287 self.with_connection(|connection| {
288 let transaction = connection
289 .unchecked_transaction()
290 .map_err(|error| database_error("failed to begin indexing transaction", error))?;
291 let existing_id = transaction
292 .query_row(
293 "SELECT id FROM indexed_entries WHERE namespace_id = ?1 AND entry_key = ?2",
294 params![namespace, key_bytes],
295 |row| row.get::<_, i64>(0),
296 )
297 .optional()
298 .map_err(|error| database_error("failed to find indexed entry", error))?;
299 let id = if let Some(id) = existing_id {
300 transaction
301 .execute(
302 "UPDATE indexed_entries
303 SET content_hash = ?1, content_len = ?2, updated_at = ?3
304 WHERE id = ?4",
305 params![hash_bytes, size_value, now_seconds(), id],
306 )
307 .map_err(|error| database_error("failed to update indexed entry", error))?;
308 transaction
309 .execute("DELETE FROM indexed_entries_fts WHERE rowid = ?1", [id])
310 .map_err(|error| database_error("failed to refresh indexed search entry", error))?;
311 id
312 } else {
313 transaction
314 .execute(
315 "INSERT INTO indexed_entries
316 (namespace_id, entry_key, content_hash, content_len, updated_at)
317 VALUES (?1, ?2, ?3, ?4, ?5)",
318 params![namespace, key_bytes, hash_bytes, size_value, now_seconds()],
319 )
320 .map_err(|error| database_error("failed to insert indexed entry", error))?;
321 transaction.last_insert_rowid()
322 };
323 transaction
324 .execute(
325 "INSERT INTO indexed_entries_fts(rowid, namespace_id, entry_key, title, tags)
326 VALUES (?1, ?2, ?3, ?4, ?5)",
327 params![id, namespace, key_text, title, ""],
328 )
329 .map_err(|error| database_error("failed to insert indexed search entry", error))?;
330 transaction
331 .commit()
332 .map_err(|error| database_error("failed to commit indexed entry", error))?;
333 Ok(IndexedEntry {
334 namespace_id,
335 key: key_bytes,
336 hash,
337 size,
338 })
339 })
340 }
341
342 pub fn remove_entry(&self, namespace_id: NamespaceId, key: impl AsRef<[u8]>) -> Result<bool> {
348 self.with_connection(|connection| {
349 let transaction = connection
350 .unchecked_transaction()
351 .map_err(|error| database_error("failed to begin indexing transaction", error))?;
352 let id = transaction
353 .query_row(
354 "SELECT id FROM indexed_entries WHERE namespace_id = ?1 AND entry_key = ?2",
355 params![namespace_id.to_string(), key.as_ref()],
356 |row| row.get::<_, i64>(0),
357 )
358 .optional()
359 .map_err(|error| database_error("failed to find indexed entry", error))?;
360 let Some(entry_id) = id else {
361 transaction
362 .commit()
363 .map_err(|error| database_error("failed to commit indexing transaction", error))?;
364 return Ok(false);
365 };
366 transaction
367 .execute("DELETE FROM indexed_entries_fts WHERE rowid = ?1", [entry_id])
368 .map_err(|error| database_error("failed to remove indexed search entry", error))?;
369 transaction
370 .execute("DELETE FROM indexed_entries WHERE id = ?1", [entry_id])
371 .map_err(|error| database_error("failed to remove indexed entry", error))?;
372 transaction
373 .commit()
374 .map_err(|error| database_error("failed to commit indexing transaction", error))?;
375 Ok(true)
376 })
377 }
378
379 pub fn search(&self, query: &str, limit: usize) -> Result<Vec<IndexedEntry>> {
388 if limit == 0 {
389 return Ok(Vec::new());
390 }
391 let limit_value = i64::try_from(limit).map_err(|error| database_error("search limit is invalid", error))?;
392 self.with_connection(|connection| {
393 let mut output = Vec::new();
394 if query.trim().is_empty() {
395 let mut statement = connection
396 .prepare(
397 "SELECT namespace_id, entry_key, content_hash, content_len
398 FROM indexed_entries ORDER BY updated_at DESC, id DESC LIMIT ?1",
399 )
400 .map_err(|error| database_error("failed to prepare index query", error))?;
401 let rows = statement
402 .query_map([limit_value], indexed_entry_from_row)
403 .map_err(|error| database_error("failed to query indexed entries", error))?;
404 for row in rows {
405 output.push(row.map_err(|error| database_error("failed to read indexed entry", error))?);
406 }
407 } else {
408 let mut statement = connection
409 .prepare(
410 "SELECT e.namespace_id, e.entry_key, e.content_hash, e.content_len
411 FROM indexed_entries_fts f
412 JOIN indexed_entries e ON e.id = f.rowid
413 WHERE indexed_entries_fts MATCH ?1
414 ORDER BY bm25(indexed_entries_fts), e.id DESC
415 LIMIT ?2",
416 )
417 .map_err(|error| database_error("failed to prepare full-text query", error))?;
418 let rows = statement
419 .query_map(params![query, limit_value], indexed_entry_from_row)
420 .map_err(|error| database_error("failed to query full-text index", error))?;
421 for row in rows {
422 output.push(row.map_err(|error| database_error("failed to read full-text result", error))?);
423 }
424 }
425 Ok(output)
426 })
427 }
428
429 pub fn enable_catalog(&self, namespace_id: NamespaceId, label: &str) -> Result<()> {
435 self.with_connection(|connection| {
436 connection
437 .execute(
438 "INSERT INTO indexed_catalogs(namespace_id, label, subscribed_at)
439 VALUES (?1, ?2, ?3)
440 ON CONFLICT(namespace_id) DO UPDATE SET label = excluded.label",
441 params![namespace_id.to_string(), label, now_seconds()],
442 )
443 .map_err(|error| database_error("failed to enable indexed catalog", error))?;
444 Ok(())
445 })
446 }
447
448 pub fn catalog_count(&self) -> Result<usize> {
454 self.with_connection(|connection| {
455 let count = connection
456 .query_row("SELECT COUNT(*) FROM indexed_catalogs", [], |row| row.get::<_, i64>(0))
457 .map_err(|error| database_error("failed to count indexed catalogs", error))?;
458 usize::try_from(count).map_err(|error| database_error("indexed catalog count is invalid", error))
459 })
460 }
461
462 pub fn load_catalogs(&self) -> Result<Vec<(NamespaceId, String)>> {
468 use std::str::FromStr;
469 self.with_connection(|connection| {
470 let mut statement = connection
471 .prepare("SELECT namespace_id, label FROM indexed_catalogs ORDER BY subscribed_at DESC")
472 .map_err(|error| database_error("failed to prepare catalog listing", error))?;
473 let rows = statement
474 .query_map([], |row| {
475 let namespace: String = row.get(0)?;
476 let label: String = row.get(1)?;
477 Ok((namespace, label))
478 })
479 .map_err(|error| database_error("failed to read indexed catalogs", error))?;
480 let mut catalogs = Vec::new();
481 for row in rows {
482 let (namespace, label) =
483 row.map_err(|error| database_error("failed to read indexed catalog record", error))?;
484 let namespace_id = NamespaceId::from_str(&namespace)
485 .map_err(|error| database_error("indexed catalog namespace is invalid", error))?;
486 catalogs.push((namespace_id, label));
487 }
488 Ok(catalogs)
489 })
490 }
491
492 pub fn catalog_entry_count(&self) -> Result<usize> {
498 self.with_connection(|connection| {
499 let count = connection
500 .query_row("SELECT COUNT(*) FROM indexed_catalog_entries", [], |row| {
501 row.get::<_, i64>(0)
502 })
503 .map_err(|error| database_error("failed to count indexed catalog entries", error))?;
504 usize::try_from(count).map_err(|error| database_error("indexed catalog entry count is invalid", error))
505 })
506 }
507
508 pub fn upsert_catalog_record(&self, record: &CatalogRecord) -> Result<CatalogRecord> {
515 record.validate()?;
516 let catalog_namespace = record.catalog_namespace_id.to_string();
517 let folder_namespace = record.folder_namespace_id.to_string();
518 let hash_bytes = record.hash.as_bytes().to_vec();
519 let size =
520 i64::try_from(record.size).map_err(|error| database_error("catalog record size is too large", error))?;
521 let tags = serde_json::to_string(&record.tags)
522 .map_err(|error| database_error("failed to serialize catalog record tags", error))?;
523 let key = record.key.clone();
524 let publisher = record.publisher.clone();
525 self.with_connection(|connection| {
526 let transaction = connection
527 .unchecked_transaction()
528 .map_err(|error| database_error("failed to begin catalog transaction", error))?;
529 let catalog_exists = transaction
530 .query_row(
531 "SELECT EXISTS(
532 SELECT 1 FROM indexed_catalogs WHERE namespace_id = ?1
533 )",
534 [&catalog_namespace],
535 |row| row.get::<_, bool>(0),
536 )
537 .map_err(|error| database_error("failed to find indexed catalog", error))?;
538 if !catalog_exists {
539 return Err(SyncwebError::InvalidConfig(format!(
540 "catalog is not enabled: {catalog_namespace}"
541 )));
542 }
543 let existing_id = transaction
544 .query_row(
545 "SELECT id FROM indexed_catalog_entries
546 WHERE catalog_namespace_id = ?1
547 AND folder_namespace_id = ?2
548 AND entry_key = ?3",
549 params![catalog_namespace, folder_namespace, key],
550 |row| row.get::<_, i64>(0),
551 )
552 .optional()
553 .map_err(|error| database_error("failed to find indexed catalog record", error))?;
554 let id = if let Some(id) = existing_id {
555 transaction
556 .execute(
557 "UPDATE indexed_catalog_entries
558 SET content_hash = ?1, content_len = ?2, folder_name = ?3,
559 title = ?4, tags = ?5, publisher = ?6, updated_at = ?7
560 WHERE id = ?8",
561 params![
562 hash_bytes,
563 size,
564 record.folder_name,
565 record.title,
566 tags,
567 publisher,
568 now_seconds(),
569 id
570 ],
571 )
572 .map_err(|error| database_error("failed to update indexed catalog record", error))?;
573 transaction
574 .execute("DELETE FROM indexed_catalog_entries_fts WHERE rowid = ?1", [id])
575 .map_err(|error| database_error("failed to refresh catalog search record", error))?;
576 id
577 } else {
578 transaction
579 .execute(
580 "INSERT INTO indexed_catalog_entries
581 (catalog_namespace_id, folder_namespace_id, entry_key, content_hash,
582 content_len, folder_name, title, tags, publisher, updated_at)
583 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
584 params![
585 catalog_namespace,
586 folder_namespace,
587 key,
588 hash_bytes,
589 size,
590 record.folder_name,
591 record.title,
592 tags,
593 publisher,
594 now_seconds()
595 ],
596 )
597 .map_err(|error| database_error("failed to insert indexed catalog record", error))?;
598 transaction.last_insert_rowid()
599 };
600 transaction
601 .execute(
602 "INSERT INTO indexed_catalog_entries_fts(
603 rowid, catalog_namespace_id, folder_namespace_id, entry_key,
604 folder_name, title, tags, publisher, content_hash
605 )
606 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
607 params![
608 id,
609 record.catalog_namespace_id.to_string(),
610 record.folder_namespace_id.to_string(),
611 String::from_utf8_lossy(&record.key),
612 record.folder_name,
613 record.title,
614 tags,
615 publisher,
616 record.hash.to_string()
617 ],
618 )
619 .map_err(|error| database_error("failed to insert catalog search record", error))?;
620 transaction
621 .commit()
622 .map_err(|error| database_error("failed to commit catalog record", error))?;
623 Ok(record.clone())
624 })
625 }
626
627 pub fn search_catalogs(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
635 if limit == 0 {
636 return Ok(Vec::new());
637 }
638 let limit_value =
639 i64::try_from(limit).map_err(|error| database_error("catalog search limit is invalid", error))?;
640 self.with_connection(|connection| {
641 let sql = if query.trim().is_empty() {
642 "SELECT catalog_namespace_id, folder_namespace_id, entry_key, content_hash,
643 content_len, folder_name, title, tags, publisher
644 FROM indexed_catalog_entries
645 ORDER BY updated_at DESC, id DESC LIMIT ?1"
646 } else {
647 "SELECT e.catalog_namespace_id, e.folder_namespace_id, e.entry_key, e.content_hash,
648 e.content_len, e.folder_name, e.title, e.tags, e.publisher
649 FROM indexed_catalog_entries_fts f
650 JOIN indexed_catalog_entries e ON e.id = f.rowid
651 WHERE indexed_catalog_entries_fts MATCH ?1
652 ORDER BY bm25(indexed_catalog_entries_fts), e.id DESC
653 LIMIT ?2"
654 };
655 let mut statement = connection
656 .prepare(sql)
657 .map_err(|error| database_error("failed to prepare catalog search", error))?;
658 let rows = if query.trim().is_empty() {
659 statement
660 .query_map([rusqlite::types::Value::Integer(limit_value)], catalog_record_from_row)
661 .map_err(|error| database_error("failed to query catalog records", error))?
662 } else {
663 statement
664 .query_map(params![query, limit_value], catalog_record_from_row)
665 .map_err(|error| database_error("failed to query catalog records", error))?
666 };
667 rows.collect::<std::result::Result<Vec<_>, _>>()
668 .map_err(|error| database_error("failed to read catalog records", error))
669 })
670 }
671
672 pub fn disable_catalog(&self, namespace_id: NamespaceId) -> Result<()> {
678 self.with_connection(|connection| {
679 let transaction = connection
680 .unchecked_transaction()
681 .map_err(|error| database_error("failed to begin catalog removal", error))?;
682 let namespace = namespace_id.to_string();
683 let mut statement = transaction
684 .prepare("SELECT id FROM indexed_catalog_entries WHERE catalog_namespace_id = ?1")
685 .map_err(|error| database_error("failed to find catalog records", error))?;
686 let ids = statement
687 .query_map([namespace.as_str()], |row| row.get::<_, i64>(0))
688 .map_err(|error| database_error("failed to list catalog records", error))?
689 .collect::<std::result::Result<Vec<_>, _>>()
690 .map_err(|error| database_error("failed to read catalog record IDs", error))?;
691 drop(statement);
692 for id in ids {
693 transaction
694 .execute("DELETE FROM indexed_catalog_entries_fts WHERE rowid = ?1", [id])
695 .map_err(|error| database_error("failed to remove catalog search record", error))?;
696 }
697 transaction
698 .execute(
699 "DELETE FROM indexed_catalog_entries WHERE catalog_namespace_id = ?1",
700 [namespace.as_str()],
701 )
702 .map_err(|error| database_error("failed to remove catalog records", error))?;
703 transaction
704 .execute(
705 "DELETE FROM indexed_catalogs WHERE namespace_id = ?1",
706 [namespace.as_str()],
707 )
708 .map_err(|error| database_error("failed to disable indexed catalog", error))?;
709 transaction
710 .commit()
711 .map_err(|error| database_error("failed to commit catalog removal", error))?;
712 Ok(())
713 })
714 }
715
716 pub fn set_channel_namespace(&self, channel_name: &str, namespace_id: NamespaceId) -> Result<()> {
722 self.with_connection(|connection| {
723 connection
724 .execute(
725 "INSERT INTO channel_namespaces(channel_name, catalog_namespace_id, created_at)
726 VALUES (?1, ?2, ?3)
727 ON CONFLICT(channel_name) DO UPDATE SET catalog_namespace_id = excluded.catalog_namespace_id",
728 params![channel_name, namespace_id.to_string(), now_seconds()],
729 )
730 .map_err(|error| database_error("failed to store channel namespace", error))?;
731 Ok(())
732 })
733 }
734
735 pub fn get_channel_namespace(&self, channel_name: &str) -> Result<Option<NamespaceId>> {
741 use std::str::FromStr;
742 self.with_connection(|connection| {
743 let result = connection
744 .query_row(
745 "SELECT catalog_namespace_id FROM channel_namespaces WHERE channel_name = ?1",
746 [channel_name],
747 |row| row.get::<_, String>(0),
748 )
749 .optional()
750 .map_err(|error| database_error("failed to look up channel namespace", error))?;
751 match result {
752 Some(ns) => {
753 let namespace_id = NamespaceId::from_str(&ns)
754 .map_err(|error| database_error("channel namespace is invalid", error))?;
755 Ok(Some(namespace_id))
756 }
757 None => Ok(None),
758 }
759 })
760 }
761
762 pub fn load_channel_namespaces(&self) -> Result<Vec<(String, NamespaceId)>> {
768 use std::str::FromStr;
769 self.with_connection(|connection| {
770 let mut statement = connection
771 .prepare("SELECT channel_name, catalog_namespace_id FROM channel_namespaces")
772 .map_err(|error| database_error("failed to prepare channel listing", error))?;
773 let rows = statement
774 .query_map([], |row| {
775 let name: String = row.get(0)?;
776 let namespace: String = row.get(1)?;
777 Ok((name, namespace))
778 })
779 .map_err(|error| database_error("failed to read channel namespaces", error))?;
780 let mut channels = Vec::new();
781 for row in rows {
782 let (name, namespace) =
783 row.map_err(|error| database_error("failed to read channel namespace row", error))?;
784 let namespace_id = NamespaceId::from_str(&namespace)
785 .map_err(|error| database_error("channel namespace is invalid", error))?;
786 channels.push((name, namespace_id));
787 }
788 Ok(channels)
789 })
790 }
791
792 pub fn enable_folder(&self, namespace_id: NamespaceId, label: &str) -> Result<()> {
798 self.with_connection(|connection| {
799 connection
800 .execute(
801 "INSERT INTO indexed_folders(namespace_id, label, enabled_at)
802 VALUES (?1, ?2, ?3)
803 ON CONFLICT(namespace_id) DO UPDATE SET label = excluded.label",
804 params![namespace_id.to_string(), label, now_seconds()],
805 )
806 .map_err(|error| database_error("failed to enable indexed folder", error))?;
807 Ok(())
808 })
809 }
810
811 pub fn disable_folder(&self, namespace_id: NamespaceId) -> Result<()> {
817 self.with_connection(|connection| {
818 let transaction = connection
819 .unchecked_transaction()
820 .map_err(|error| database_error("failed to begin folder removal", error))?;
821 let namespace = namespace_id.to_string();
822 let mut statement = transaction
823 .prepare("SELECT id FROM indexed_entries WHERE namespace_id = ?1")
824 .map_err(|error| database_error("failed to find folder entries", error))?;
825 let ids = statement
826 .query_map([namespace.as_str()], |row| row.get::<_, i64>(0))
827 .map_err(|error| database_error("failed to list folder entries", error))?
828 .collect::<std::result::Result<Vec<_>, _>>()
829 .map_err(|error| database_error("failed to read folder entries", error))?;
830 drop(statement);
831 for id in ids {
832 transaction
833 .execute("DELETE FROM indexed_entries_fts WHERE rowid = ?1", [id])
834 .map_err(|error| database_error("failed to remove folder search entry", error))?;
835 }
836 transaction
837 .execute(
838 "DELETE FROM indexed_entries WHERE namespace_id = ?1",
839 [namespace.as_str()],
840 )
841 .map_err(|error| database_error("failed to remove folder entries", error))?;
842 transaction
843 .execute(
844 "DELETE FROM indexed_folders WHERE namespace_id = ?1",
845 [namespace.as_str()],
846 )
847 .map_err(|error| database_error("failed to disable indexed folder", error))?;
848 transaction
849 .commit()
850 .map_err(|error| database_error("failed to commit folder removal", error))?;
851 Ok(())
852 })
853 }
854
855 fn with_connection<T>(&self, operation: impl FnOnce(&mut Connection) -> Result<T>) -> Result<T> {
856 let mut connection = self
857 .connection
858 .lock()
859 .map_err(|error| SyncwebError::operation("indexing database lock poisoned", error))?;
860 operation(&mut connection)
861 }
862
863 pub fn save_denylist_rules(&self, rules: &[DenylistRule]) -> Result<()> {
869 self.with_connection(|connection| {
870 connection
871 .execute("DELETE FROM denylist_rules", [])
872 .map_err(|error| database_error("failed to clear denylist rules", error))?;
873 let now = now_seconds();
874 for rule in rules {
875 let (rule_type, rule_value, namespace_id) = match rule {
876 DenylistRule::Device(d) => ("device", d.as_bytes().to_vec(), None::<String>),
877 DenylistRule::Hash(h) => ("hash", h.as_bytes().to_vec(), None),
878 DenylistRule::File { namespace_id: ns, key } => ("file", key.clone(), ns.map(|n| n.to_string())),
879 };
880 connection
881 .execute(
882 "INSERT OR REPLACE INTO denylist_rules(rule_type, rule_value, namespace_id, updated_at)
883 VALUES (?1, ?2, ?3, ?4)",
884 params![rule_type, rule_value, namespace_id, now],
885 )
886 .map_err(|error| database_error("failed to save denylist rule", error))?;
887 }
888 Ok(())
889 })
890 }
891
892 pub fn load_denylist_rules(&self) -> Result<Vec<DenylistRule>> {
898 self.with_connection(|connection| {
899 let mut stmt = connection
900 .prepare("SELECT rule_type, rule_value, namespace_id FROM denylist_rules ORDER BY updated_at")
901 .map_err(|error| database_error("failed to prepare denylist query", error))?;
902 let rules = stmt
903 .query_map([], |row| {
904 let rule_type: String = row.get(0)?;
905 let rule_value: Vec<u8> = row.get(1)?;
906 let namespace_id: Option<String> = row.get(2)?;
907 match rule_type.as_str() {
908 "device" => Ok(DenylistRule::Device(String::from_utf8_lossy(&rule_value).to_string())),
909 "hash" => {
910 let arr: [u8; 32] = rule_value.try_into().map_err(|error: Vec<u8>| {
911 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
912 "invalid hash length",
913 format!("expected 32 bytes, got {}", error.len()),
914 )))
915 })?;
916 Ok(DenylistRule::Hash(Hash::from(arr)))
917 }
918 "file" => {
919 let ns = namespace_id.and_then(|s| s.parse::<NamespaceId>().ok());
920 Ok(DenylistRule::File {
921 namespace_id: ns,
922 key: rule_value,
923 })
924 }
925 _ => Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
926 SyncwebError::operation("unknown rule type", rule_type),
927 ))),
928 }
929 })
930 .map_err(|error| database_error("failed to query denylist rules", error))?
931 .collect::<std::result::Result<Vec<_>, _>>()
932 .map_err(|error| database_error("failed to read denylist rules", error))?;
933 Ok(rules)
934 })
935 }
936
937 pub fn load_filter_lists(&self) -> Result<Vec<denylist::FilterList>> {
943 self.with_connection(|connection| {
944 let mut stmt = connection
945 .prepare("SELECT namespace_id, sequence, publisher, payload, updated_at FROM filter_lists ORDER BY updated_at")
946 .map_err(|error| database_error("failed to prepare filter lists query", error))?;
947 let lists = stmt
948 .query_map([], |row| {
949 let payload: Vec<u8> = row.get(3)?;
950 let list: denylist::FilterList = serde_json::from_slice(&payload).map_err(|error| {
951 rusqlite::Error::ToSqlConversionFailure(Box::new(SyncwebError::operation(
952 "failed to deserialize filter list",
953 error,
954 )))
955 })?;
956 Ok(list)
957 })
958 .map_err(|error| database_error("failed to query filter lists", error))?
959 .collect::<std::result::Result<Vec<_>, _>>()
960 .map_err(|error| database_error("failed to read filter list rows", error))?;
961 Ok(lists)
962 })
963 }
964
965 pub fn upsert_filter_list(&self, list: &denylist::FilterList) -> Result<()> {
971 let payload = list.to_bytes()?;
972 self.with_connection(|connection| {
973 connection
974 .execute(
975 "INSERT OR REPLACE INTO filter_lists(namespace_id, sequence, publisher, payload, updated_at)
976 VALUES (?1, ?2, ?3, ?4, ?5)",
977 params![
978 list.namespace_id.to_string(),
979 i64::try_from(list.sequence).unwrap_or(i64::MAX),
980 list.publisher,
981 payload,
982 now_seconds(),
983 ],
984 )
985 .map_err(|error| database_error("failed to upsert filter list", error))?;
986 Ok(())
987 })
988 }
989
990 pub fn load_links(&self) -> Result<(Vec<links::MutablePointer>, Vec<String>, Vec<links::PrivateLink>)> {
996 self.with_connection(|connection| {
997 let mut stmt = connection
998 .prepare("SELECT link, kind, payload, updated_at FROM stable_links ORDER BY updated_at")
999 .map_err(|error| database_error("failed to prepare links query", error))?;
1000 let mut pointers = Vec::new();
1001 let rows = stmt
1002 .query_map([], |row| {
1003 let kind: String = row.get(1)?;
1004 let payload: Vec<u8> = row.get(2)?;
1005 Ok((kind, payload))
1006 })
1007 .map_err(|error| database_error("failed to query stable links", error))?
1008 .collect::<std::result::Result<Vec<_>, _>>()
1009 .map_err(|error| database_error("failed to read link rows", error))?;
1010 for (kind, payload) in rows {
1011 if kind.as_str() == "mutable"
1012 && let Ok(p) = serde_json::from_slice::<links::MutablePointer>(&payload)
1013 {
1014 pointers.push(p);
1015 }
1016 }
1017
1018 let mut mirror_stmt = connection
1019 .prepare("SELECT ticket FROM link_mirrors ORDER BY priority")
1020 .map_err(|error| database_error("failed to prepare mirror query", error))?;
1021 let mirrors = mirror_stmt
1022 .query_map([], |row| row.get::<_, String>(0))
1023 .map_err(|error| database_error("failed to query mirrors", error))?
1024 .collect::<std::result::Result<Vec<_>, _>>()
1025 .map_err(|error| database_error("failed to read mirror rows", error))?;
1026
1027 let mut revoked = Vec::new();
1028 if let Ok(mut rev_stmt) = connection.prepare("SELECT manifest, capability, revoked_at FROM revoked_links")
1029 && let Ok(rev_rows) = rev_stmt.query_map([], |row| {
1030 let manifest_bytes: Vec<u8> = row.get(0)?;
1031 let capability_blob: Vec<u8> = row.get(1)?;
1032 if manifest_bytes.len() != 32 {
1033 return Err(rusqlite::Error::InvalidColumnType(
1034 0,
1035 "manifest must be 32 bytes".to_owned(),
1036 rusqlite::types::Type::Blob,
1037 ));
1038 }
1039 if capability_blob.len() != 32 {
1040 return Err(rusqlite::Error::InvalidColumnType(
1041 1,
1042 "capability must be 32 bytes".to_owned(),
1043 rusqlite::types::Type::Blob,
1044 ));
1045 }
1046 let Ok(manifests) = manifest_bytes.try_into() else {
1047 return Err(rusqlite::Error::InvalidColumnType(
1048 0,
1049 "manifest conversion failed despite length check".to_owned(),
1050 rusqlite::types::Type::Blob,
1051 ));
1052 };
1053 let hash = Hash::from_bytes(manifests);
1054 let Ok(cap) = capability_blob.try_into() else {
1055 return Err(rusqlite::Error::InvalidColumnType(
1056 1,
1057 "capability conversion failed despite length check".to_owned(),
1058 rusqlite::types::Type::Blob,
1059 ));
1060 };
1061 Ok((hash, cap))
1062 })
1063 {
1064 for row in rev_rows.flatten() {
1065 if let Ok(link) = PrivateLink::new(row.0, row.1, u64::MAX) {
1066 revoked.push(link);
1067 }
1068 }
1069 }
1070 Ok((pointers, mirrors, revoked))
1071 })
1072 }
1073
1074 pub fn save_links(
1080 &self,
1081 pointers: &[links::MutablePointer],
1082 mirrors: &[String],
1083 revoked: &[links::PrivateLink],
1084 ) -> Result<()> {
1085 self.with_connection(|connection| {
1086 connection.execute("DELETE FROM stable_links", [])
1087 .map_err(|error| database_error("failed to clear stable links", error))?;
1088 connection.execute("DELETE FROM link_mirrors", [])
1089 .map_err(|error| database_error("failed to clear link mirrors", error))?;
1090 connection.execute("DELETE FROM revoked_links", [])
1091 .map_err(|error| database_error("failed to clear revoked links", error))?;
1092 let now = now_seconds();
1093 for pointer in pointers {
1094 let payload = serde_json::to_vec(pointer)
1095 .map_err(|error| database_error("failed to serialize pointer", error))?;
1096 connection.execute(
1097 "INSERT OR REPLACE INTO stable_links(link, kind, publisher, alias, content_hash, sequence, version, payload, updated_at)
1098 VALUES (?1, 'mutable', ?2, ?3, NULL, ?4, ?5, ?6, ?7)",
1099 params![
1100 format!("mutable:{}:{}", pointer.publisher, pointer.alias),
1101 pointer.publisher.to_string(),
1102 pointer.alias,
1103 i64::try_from(pointer.sequence).unwrap_or(i64::MAX),
1104 pointer.version.as_deref(),
1105 payload,
1106 now,
1107 ],
1108 ).map_err(|error| database_error("failed to save pointer", error))?;
1109 }
1110 for uri in mirrors {
1111 connection.execute(
1112 "INSERT INTO link_mirrors(link, provider, ticket, priority, updated_at)
1113 VALUES ('', 'local', ?1, 0, ?2)",
1114 params![uri, now],
1115 ).map_err(|error| database_error("failed to save mirror", error))?;
1116 }
1117 for link in revoked {
1118 connection.execute(
1119 "INSERT OR REPLACE INTO revoked_links(manifest, capability, revoked_at)
1120 VALUES (?1, ?2, ?3)",
1121 params![link.manifest.as_bytes(), link.capability.as_slice(), now],
1122 ).map_err(|error| database_error("failed to save revoked link", error))?;
1123 }
1124 Ok(())
1125 })
1126 }
1127
1128 pub fn upsert_link_pointer(&self, pointer: &links::MutablePointer) -> Result<()> {
1134 let payload =
1135 serde_json::to_vec(pointer).map_err(|error| database_error("failed to serialize pointer", error))?;
1136 self.with_connection(|connection| {
1137 connection.execute(
1138 "INSERT OR REPLACE INTO stable_links(link, kind, publisher, alias, content_hash, sequence, version, payload, updated_at)
1139 VALUES (?1, 'mutable', ?2, ?3, NULL, ?4, ?5, ?6, ?7)",
1140 params![
1141 format!("mutable:{}:{}", pointer.publisher, pointer.alias),
1142 pointer.publisher.to_string(),
1143 pointer.alias,
1144 i64::try_from(pointer.sequence).unwrap_or(i64::MAX),
1145 pointer.version.as_deref(),
1146 payload,
1147 now_seconds(),
1148 ],
1149 )
1150 .map_err(|error| database_error("failed to upsert link pointer", error))?;
1151 Ok(())
1152 })
1153 }
1154
1155 pub fn upsert_link_mirror(&self, ticket: &str) -> Result<()> {
1161 self.with_connection(|connection| {
1162 connection
1163 .execute(
1164 "INSERT OR REPLACE INTO link_mirrors(link, provider, ticket, priority, updated_at)
1165 VALUES ('', 'local', ?1, 0, ?2)",
1166 params![ticket, now_seconds()],
1167 )
1168 .map_err(|error| database_error("failed to upsert link mirror", error))?;
1169 Ok(())
1170 })
1171 }
1172}
1173
1174#[derive(Clone)]
1176pub struct IndexingService {
1177 database: IndexingDatabase,
1178 events: broadcast::Sender<IndexingEvent>,
1179 tasks: Arc<Mutex<HashMap<NamespaceId, JoinHandle<()>>>>,
1180 denylist: denylist::DenylistService,
1181}
1182
1183impl std::fmt::Debug for IndexingService {
1184 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1185 formatter
1186 .debug_struct("IndexingService")
1187 .field("database", &self.database)
1188 .finish_non_exhaustive()
1189 }
1190}
1191
1192impl IndexingService {
1193 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
1199 Self::with_database(IndexingDatabase::open(path)?)
1200 }
1201
1202 pub fn in_memory() -> Result<Self> {
1208 Self::with_database(IndexingDatabase::in_memory()?)
1209 }
1210
1211 pub fn with_database(database: IndexingDatabase) -> Result<Self> {
1217 let (events, _) = broadcast::channel(EVENT_CAPACITY);
1218 let denylist = denylist::DenylistService::with_database(database.clone())?;
1219 Ok(Self {
1220 database,
1221 events,
1222 tasks: Arc::new(Mutex::new(HashMap::new())),
1223 denylist,
1224 })
1225 }
1226
1227 #[must_use]
1229 pub fn catalog_service(
1230 &self,
1231 docs: &crate::node::docs_engine::DocsEngine,
1232 blobs: &crate::node::blob_store::BlobStore,
1233 author: iroh_docs::AuthorId,
1234 ) -> CatalogService {
1235 CatalogService::new(self, docs, blobs, author)
1236 }
1237
1238 #[must_use]
1240 pub fn denylist_service(&self) -> denylist::DenylistService {
1241 self.denylist.clone()
1242 }
1243
1244 #[must_use]
1245 pub const fn database(&self) -> &IndexingDatabase {
1246 &self.database
1247 }
1248
1249 pub fn search_local(&self, query: &str, limit: usize) -> Result<Vec<IndexedEntry>> {
1255 self.database.search(query, limit)
1256 }
1257
1258 pub fn search_global(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
1264 self.database.search_catalogs(query, limit)
1265 }
1266
1267 pub fn search(&self, query: &str, limit: usize) -> Result<Vec<CatalogRecord>> {
1276 Ok(self
1277 .search_global(query, limit)?
1278 .into_iter()
1279 .filter(|record| self.denylist.check_discovery(record).is_ok())
1280 .collect())
1281 }
1282
1283 #[must_use]
1285 pub fn subscribe(&self) -> broadcast::Receiver<IndexingEvent> {
1286 self.events.subscribe()
1287 }
1288
1289 pub async fn enable_folder(&self, folder: &SyncwebFolder) -> Result<IndexingHandle> {
1297 let namespace_id = folder.namespace_id();
1298 if self
1299 .tasks
1300 .lock()
1301 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
1302 .get(&namespace_id)
1303 .is_some_and(|task| !task.is_finished())
1304 {
1305 return Ok(IndexingHandle { namespace_id });
1306 }
1307
1308 let live_events = folder.docs_engine().watch_and_sync(folder.doc()).await?;
1309 self.database.enable_folder(namespace_id, &namespace_id.to_string())?;
1310 send_event(&self.events, IndexingEvent::FolderEnabled { namespace_id });
1311
1312 for entry in folder.docs_engine().list_latest(folder.doc()).await? {
1313 let indexed = self.index_entry(namespace_id, &entry)?;
1314 send_event(&self.events, IndexingEvent::EntryIndexed(indexed));
1315 }
1316
1317 let database = self.database.clone();
1318 let events = self.events.clone();
1319 let task = tokio::spawn(async move {
1320 consume_folder_events(namespace_id, live_events, database, events).await;
1321 });
1322 self.tasks
1323 .lock()
1324 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
1325 .insert(namespace_id, task);
1326 Ok(IndexingHandle { namespace_id })
1327 }
1328
1329 pub async fn disable_folder(&self, namespace_id: NamespaceId) -> Result<()> {
1335 let task_handle = self
1336 .tasks
1337 .lock()
1338 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
1339 .remove(&namespace_id);
1340 if let Some(task) = task_handle {
1341 task.abort();
1342 }
1343 let database = self.database.clone();
1344 tokio::task::spawn_blocking(move || database.disable_folder(namespace_id))
1345 .await
1346 .map_err(|error| SyncwebError::operation("indexing folder removal task failed", error))??;
1347 send_event(&self.events, IndexingEvent::FolderDisabled { namespace_id });
1348 Ok(())
1349 }
1350
1351 pub fn is_folder_enabled(&self, namespace_id: NamespaceId) -> Result<bool> {
1357 let active_task = self
1358 .tasks
1359 .lock()
1360 .map_err(|error| SyncwebError::operation("indexing task lock poisoned", error))?
1361 .get(&namespace_id)
1362 .is_some_and(|task| !task.is_finished());
1363 Ok(active_task || self.database.is_folder_enabled(namespace_id)?)
1364 }
1365
1366 pub fn index_entry(&self, namespace_id: NamespaceId, entry: &Entry) -> Result<IndexedEntry> {
1376 if !self.database.is_folder_enabled(namespace_id)? {
1377 return Err(SyncwebError::FolderNotFound(namespace_id.to_string()));
1378 }
1379 self.database
1380 .upsert_entry(namespace_id, entry.key(), entry.content_hash(), entry.content_len())
1381 }
1382}
1383
1384async fn consume_folder_events(
1385 namespace_id: NamespaceId,
1386 mut live_events: impl n0_future::Stream<Item = Result<LiveEvent>> + Send + Unpin + 'static,
1387 database: IndexingDatabase,
1388 events: broadcast::Sender<IndexingEvent>,
1389) {
1390 while let Some(event_result) = live_events.next().await {
1391 let event = match event_result {
1392 Ok(event) => event,
1393 Err(error) => {
1394 send_event(
1395 &events,
1396 IndexingEvent::Error {
1397 namespace_id,
1398 message: error.to_string(),
1399 },
1400 );
1401 break;
1402 }
1403 };
1404 match event {
1405 LiveEvent::InsertLocal { entry } | LiveEvent::InsertRemote { entry, .. } => {
1406 match database.upsert_entry(namespace_id, entry.key(), entry.content_hash(), entry.content_len()) {
1407 Ok(indexed) => send_event(&events, IndexingEvent::EntryIndexed(indexed)),
1408 Err(error) => {
1409 send_event(
1410 &events,
1411 IndexingEvent::Error {
1412 namespace_id,
1413 message: error.to_string(),
1414 },
1415 );
1416 }
1417 }
1418 }
1419 LiveEvent::ContentReady { hash } => send_event(
1420 &events,
1421 IndexingEvent::Core {
1422 namespace_id,
1423 event: CoreIndexingEvent::ContentReady { hash },
1424 },
1425 ),
1426 LiveEvent::PendingContentReady => send_event(
1427 &events,
1428 IndexingEvent::Core {
1429 namespace_id,
1430 event: CoreIndexingEvent::PendingContentReady,
1431 },
1432 ),
1433 LiveEvent::NeighborUp(_) => send_event(
1434 &events,
1435 IndexingEvent::Core {
1436 namespace_id,
1437 event: CoreIndexingEvent::NeighborUp,
1438 },
1439 ),
1440 LiveEvent::NeighborDown(_) => send_event(
1441 &events,
1442 IndexingEvent::Core {
1443 namespace_id,
1444 event: CoreIndexingEvent::NeighborDown,
1445 },
1446 ),
1447 LiveEvent::SyncFinished(_) => send_event(
1448 &events,
1449 IndexingEvent::Core {
1450 namespace_id,
1451 event: CoreIndexingEvent::SyncFinished,
1452 },
1453 ),
1454 }
1455 }
1456}
1457
1458fn indexed_entry_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<IndexedEntry> {
1459 let namespace = row.get::<_, String>(0)?;
1460 let namespace_id = NamespaceId::from_str(&namespace).map_err(|error| {
1461 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
1462 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(conversion_error))
1463 })?;
1464 let key = row.get(1)?;
1465 let hash_bytes = row.get::<_, Vec<u8>>(2)?;
1466 let hash_array = <[u8; 32]>::try_from(hash_bytes.as_slice())
1467 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(2, rusqlite::types::Type::Blob, Box::new(error)))?;
1468 let size_value = row.get::<_, i64>(3)?;
1469 let size = u64::try_from(size_value).map_err(|error| {
1470 rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Integer, Box::new(error))
1471 })?;
1472 Ok(IndexedEntry {
1473 namespace_id,
1474 key,
1475 hash: Hash::from_bytes(hash_array),
1476 size,
1477 })
1478}
1479
1480fn catalog_record_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<CatalogRecord> {
1481 let catalog_namespace = row.get::<_, String>(0)?;
1482 let catalog_namespace_id = NamespaceId::from_str(&catalog_namespace).map_err(|error| {
1483 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
1484 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(conversion_error))
1485 })?;
1486 let folder_namespace = row.get::<_, String>(1)?;
1487 let folder_namespace_id = NamespaceId::from_str(&folder_namespace).map_err(|error| {
1488 let conversion_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string());
1489 rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, Box::new(conversion_error))
1490 })?;
1491 let hash_bytes = row.get::<_, Vec<u8>>(3)?;
1492 let hash_array = <[u8; 32]>::try_from(hash_bytes.as_slice())
1493 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Blob, Box::new(error)))?;
1494 let size_value = row.get::<_, i64>(4)?;
1495 let size = u64::try_from(size_value).map_err(|error| {
1496 rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Integer, Box::new(error))
1497 })?;
1498 let tags_json = row.get::<_, String>(7)?;
1499 let tags = serde_json::from_str(&tags_json)
1500 .map_err(|error| rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(error)))?;
1501 Ok(CatalogRecord {
1502 catalog_namespace_id,
1503 folder_namespace_id,
1504 key: row.get(2)?,
1505 hash: Hash::from_bytes(hash_array),
1506 size,
1507 folder_name: row.get(5)?,
1508 title: row.get(6)?,
1509 tags,
1510 publisher: row.get(8)?,
1511 })
1512}
1513
1514const SCHEMA_PART1: &str = "PRAGMA journal_mode = WAL;
1515 CREATE TABLE IF NOT EXISTS index_metadata (
1516 key TEXT PRIMARY KEY NOT NULL,
1517 value TEXT NOT NULL
1518 );
1519 CREATE TABLE IF NOT EXISTS indexed_folders (
1520 namespace_id TEXT PRIMARY KEY NOT NULL,
1521 label TEXT NOT NULL,
1522 enabled_at INTEGER NOT NULL
1523 );
1524 CREATE TABLE IF NOT EXISTS indexed_entries (
1525 id INTEGER PRIMARY KEY,
1526 namespace_id TEXT NOT NULL REFERENCES indexed_folders(namespace_id) ON DELETE CASCADE,
1527 entry_key BLOB NOT NULL,
1528 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
1529 content_len INTEGER NOT NULL CHECK(content_len >= 0),
1530 updated_at INTEGER NOT NULL,
1531 UNIQUE(namespace_id, entry_key)
1532 );
1533 CREATE INDEX IF NOT EXISTS indexed_entries_namespace
1534 ON indexed_entries(namespace_id);
1535 CREATE VIRTUAL TABLE IF NOT EXISTS indexed_entries_fts USING fts5(
1536 namespace_id UNINDEXED,
1537 entry_key,
1538 title,
1539 tags,
1540 tokenize = 'unicode61'
1541 );
1542 CREATE TABLE IF NOT EXISTS indexed_catalogs (
1543 namespace_id TEXT PRIMARY KEY NOT NULL,
1544 label TEXT NOT NULL,
1545 subscribed_at INTEGER NOT NULL
1546 );
1547 CREATE TABLE IF NOT EXISTS indexed_catalog_entries (
1548 id INTEGER PRIMARY KEY,
1549 catalog_namespace_id TEXT NOT NULL REFERENCES indexed_catalogs(namespace_id) ON DELETE CASCADE,
1550 folder_namespace_id TEXT NOT NULL,
1551 entry_key BLOB NOT NULL,
1552 content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
1553 content_len INTEGER NOT NULL CHECK(content_len >= 0),
1554 folder_name TEXT NOT NULL,
1555 title TEXT NOT NULL,
1556 tags TEXT NOT NULL,
1557 publisher TEXT NOT NULL,
1558 updated_at INTEGER NOT NULL,
1559 UNIQUE(catalog_namespace_id, folder_namespace_id, entry_key)
1560 );
1561 CREATE INDEX IF NOT EXISTS indexed_catalog_entries_catalog
1562 ON indexed_catalog_entries(catalog_namespace_id);
1563 CREATE VIRTUAL TABLE IF NOT EXISTS indexed_catalog_entries_fts USING fts5(
1564 catalog_namespace_id UNINDEXED,
1565 folder_namespace_id UNINDEXED,
1566 entry_key,
1567 folder_name,
1568 title,
1569 tags,
1570 publisher,
1571 content_hash,
1572 tokenize = 'unicode61'
1573 );";
1574
1575const SCHEMA_PART2: &str = "CREATE TABLE IF NOT EXISTS stable_links (
1576 link TEXT PRIMARY KEY NOT NULL,
1577 kind TEXT NOT NULL,
1578 publisher TEXT,
1579 alias TEXT,
1580 content_hash BLOB CHECK(content_hash IS NULL OR length(content_hash) = 32),
1581 sequence INTEGER NOT NULL CHECK(sequence >= 0),
1582 version TEXT,
1583 payload BLOB NOT NULL,
1584 updated_at INTEGER NOT NULL
1585 );
1586CREATE TABLE IF NOT EXISTS link_mirrors (
1587link TEXT NOT NULL,
1588provider TEXT NOT NULL,
1589ticket TEXT NOT NULL,
1590priority INTEGER NOT NULL DEFAULT 0,
1591updated_at INTEGER NOT NULL,
1592PRIMARY KEY(link, provider)
1593);
1594CREATE TABLE IF NOT EXISTS revoked_links (
1595 manifest BLOB NOT NULL CHECK(length(manifest) = 32),
1596 capability TEXT NOT NULL,
1597 revoked_at INTEGER NOT NULL,
1598 PRIMARY KEY(manifest, capability)
1599);
1600 CREATE TABLE IF NOT EXISTS denylist_rules (
1601 rule_type TEXT NOT NULL,
1602 rule_value BLOB NOT NULL,
1603 namespace_id TEXT,
1604 updated_at INTEGER NOT NULL,
1605 PRIMARY KEY(rule_type, rule_value, namespace_id)
1606 );
1607 CREATE TABLE IF NOT EXISTS filter_lists (
1608 namespace_id TEXT PRIMARY KEY NOT NULL,
1609 sequence INTEGER NOT NULL CHECK(sequence > 0),
1610 publisher TEXT NOT NULL,
1611 payload BLOB NOT NULL,
1612 updated_at INTEGER NOT NULL
1613 );";
1614
1615const SCHEMA_PART3: &str = "CREATE TABLE IF NOT EXISTS channel_namespaces (
1616 channel_name TEXT PRIMARY KEY NOT NULL,
1617 catalog_namespace_id TEXT NOT NULL,
1618 created_at INTEGER NOT NULL
1619 );";
1620
1621fn initialize_connection(connection: &Connection) -> Result<()> {
1622 connection
1623 .busy_timeout(std::time::Duration::from_secs(5))
1624 .map_err(|error| database_error("failed to configure indexing database", error))?;
1625 connection
1626 .pragma_update(None, "foreign_keys", "ON")
1627 .map_err(|error| database_error("failed to enable indexing foreign keys", error))?;
1628 initialize_schema(connection)?;
1629 connection
1630 .execute(
1631 "INSERT INTO index_metadata(key, value)
1632 VALUES ('schema_version', ?1)
1633 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
1634 [SCHEMA_VERSION],
1635 )
1636 .map_err(|error| database_error("failed to persist indexing schema version", error))?;
1637 Ok(())
1638}
1639
1640fn initialize_schema(connection: &Connection) -> Result<()> {
1641 for part in [SCHEMA_PART1, SCHEMA_PART2, SCHEMA_PART3] {
1642 connection
1643 .execute_batch(part)
1644 .map_err(|error| database_error("failed to initialize indexing database schema", error))?;
1645 }
1646 Ok(())
1647}
1648
1649fn now_seconds() -> i64 {
1650 SystemTime::now()
1651 .duration_since(UNIX_EPOCH)
1652 .map_or(0, |duration| i64::try_from(duration.as_secs()).unwrap_or(i64::MAX))
1653}
1654
1655fn database_error(context: &'static str, error: impl std::fmt::Display) -> SyncwebError {
1656 SyncwebError::operation(context, error)
1657}
1658
1659fn send_event(events: &broadcast::Sender<IndexingEvent>, event: IndexingEvent) {
1660 let _ = events.send(event);
1661}