1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat(collections): visibility-filtered entry queries + collection helpers

This commit is contained in:
TheGeneralist 2026-06-26 16:51:48 +02:00
parent ab054f6256
commit 02cab149fa
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4

View file

@ -85,6 +85,15 @@ pub struct TagNode {
pub children: Vec<TagNode>,
}
#[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<Option<PathBuf>> {
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<Vec<EntrySummary>> {
pub fn list_root_entries(conn: &rusqlite::Connection, caller_bits: u32) -> Result<Vec<EntrySummary>> {
let mut stmt = conn.prepare(
"SELECT
e.entry_uid,
@ -203,12 +212,20 @@ pub fn list_root_entries(conn: &rusqlite::Connection) -> Result<Vec<EntrySummary
LEFT JOIN entry_artifacts ea ON ea.entry_id = e.id
LEFT JOIN blobs b ON b.id = ea.blob_id
WHERE e.parent_entry_id IS NULL
AND (
CAST(?1 AS INTEGER) & 12 != 0
OR EXISTS (
SELECT 1 FROM collection_entries ce
WHERE ce.entry_id = e.id
AND ce.visibility_bits & CAST(?1 AS INTEGER) != 0
)
)
GROUP BY e.id
ORDER BY e.archived_at DESC, e.id DESC",
)?;
let entries = stmt
.query_map([], |row| {
.query_map([caller_bits as i64], |row| {
Ok(EntrySummary {
entry_uid: row.get(0)?,
archived_at: row.get(1)?,
@ -252,7 +269,7 @@ pub fn get_entry_detail(
return Ok(None);
};
let summary = list_root_entries(conn)?
let summary = list_root_entries(conn, u32::MAX)?
.into_iter()
.find(|entry| entry.entry_uid == entry_uid)
.context("entry disappeared while loading detail")?;
@ -325,6 +342,56 @@ pub fn get_capture_job(
}))
}
/// Lists all collections in the archive.
pub fn list_collections(conn: &rusqlite::Connection) -> Result<Vec<CollectionSummary>> {
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<Option<Vec<EntryCollectionMembership>>> {
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<String>,
/// Tag full_path filter; includes all entries (root + child) matching the tag subtree
pub tag: Option<String>,
/// 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)?;