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:
parent
ab054f6256
commit
02cab149fa
1 changed files with 82 additions and 3 deletions
|
|
@ -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)?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue