diff --git a/crates/archivr-core/src/archive.rs b/crates/archivr-core/src/archive.rs index 23eeb33..89235bc 100644 --- a/crates/archivr-core/src/archive.rs +++ b/crates/archivr-core/src/archive.rs @@ -85,6 +85,15 @@ pub struct TagNode { pub children: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct CollectionSummary { + pub collection_uid: String, + pub name: String, + pub slug: String, + pub default_visibility_bits: u32, + pub created_at: String, +} + pub fn find_archive_path_from(start: &Path) -> Result> { let mut dir = start.to_path_buf(); loop { @@ -184,7 +193,7 @@ pub fn initialize_store_directories(store_path: &Path) -> Result<()> { Ok(()) } -pub fn list_root_entries(conn: &rusqlite::Connection) -> Result> { +pub fn list_root_entries(conn: &rusqlite::Connection, caller_bits: u32) -> Result> { let mut stmt = conn.prepare( "SELECT e.entry_uid, @@ -203,12 +212,20 @@ pub fn list_root_entries(conn: &rusqlite::Connection) -> Result Result> { + let records = database::list_collections(conn)?; + Ok(records + .into_iter() + .map(|r| CollectionSummary { + collection_uid: r.collection_uid, + name: r.name, + slug: r.slug, + default_visibility_bits: r.default_visibility_bits, + created_at: r.created_at, + }) + .collect()) +} + +/// Represents an entry's membership in a collection with its visibility bits. +#[derive(Debug, Clone, serde::Serialize)] +pub struct EntryCollectionMembership { + pub collection_uid: String, + pub visibility_bits: u32, +} + +/// Returns collection memberships for the given entry_uid. +/// Returns Ok(None) if the entry_uid does not exist. +pub fn get_entry_collections( + conn: &rusqlite::Connection, + entry_uid: &str, +) -> Result>> { + let Some(entry_id) = conn + .query_row( + "SELECT id FROM archived_entries WHERE entry_uid = ?1", + [entry_uid], + |row| row.get::<_, i64>(0), + ) + .optional()? + else { + return Ok(None); + }; + let memberships = database::get_entry_collection_memberships(conn, entry_id)?; + Ok(Some( + memberships + .into_iter() + .map(|(_, uid, bits)| EntryCollectionMembership { + collection_uid: uid, + visibility_bits: bits, + }) + .collect(), + )) +} + /// Resolves an artifact to its absolute on-disk path under `store_path`. /// /// `artifact.relpath` is a store-relative path (e.g. `raw/a/b/abc.pdf`). @@ -368,6 +435,9 @@ pub struct SearchEntriesQuery { pub before: Option, /// Tag full_path filter; includes all entries (root + child) matching the tag subtree pub tag: Option, + /// Role bits of the caller for visibility filtering. Admins (bits 4/8) bypass all filters. + /// Pass `u32::MAX` internally to bypass all visibility. Pass 0 for unauthenticated guests only. + pub caller_bits: u32, } /// Parses a raw search string into a [`SearchEntriesQuery`]. @@ -498,6 +568,15 @@ pub fn search_entries( params.push(b.clone()); } + // Visibility filter + let n = params.len() + 1; + sql.push_str(&format!( + " AND (CAST(?{n} AS INTEGER) & 12 != 0 \ + OR EXISTS (SELECT 1 FROM collection_entries ce \ + WHERE ce.entry_id = e.id AND ce.visibility_bits & CAST(?{n} AS INTEGER) != 0))" + )); + params.push(query.caller_bits.to_string()); + sql.push_str(" GROUP BY e.id ORDER BY e.archived_at DESC, e.id DESC"); let mut stmt = conn.prepare(&sql)?;