mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-22 03:05:32 +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>,
|
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>> {
|
pub fn find_archive_path_from(start: &Path) -> Result<Option<PathBuf>> {
|
||||||
let mut dir = start.to_path_buf();
|
let mut dir = start.to_path_buf();
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -184,7 +193,7 @@ pub fn initialize_store_directories(store_path: &Path) -> Result<()> {
|
||||||
Ok(())
|
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(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT
|
"SELECT
|
||||||
e.entry_uid,
|
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 entry_artifacts ea ON ea.entry_id = e.id
|
||||||
LEFT JOIN blobs b ON b.id = ea.blob_id
|
LEFT JOIN blobs b ON b.id = ea.blob_id
|
||||||
WHERE e.parent_entry_id IS NULL
|
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
|
GROUP BY e.id
|
||||||
ORDER BY e.archived_at DESC, e.id DESC",
|
ORDER BY e.archived_at DESC, e.id DESC",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let entries = stmt
|
let entries = stmt
|
||||||
.query_map([], |row| {
|
.query_map([caller_bits as i64], |row| {
|
||||||
Ok(EntrySummary {
|
Ok(EntrySummary {
|
||||||
entry_uid: row.get(0)?,
|
entry_uid: row.get(0)?,
|
||||||
archived_at: row.get(1)?,
|
archived_at: row.get(1)?,
|
||||||
|
|
@ -252,7 +269,7 @@ pub fn get_entry_detail(
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
let summary = list_root_entries(conn)?
|
let summary = list_root_entries(conn, u32::MAX)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|entry| entry.entry_uid == entry_uid)
|
.find(|entry| entry.entry_uid == entry_uid)
|
||||||
.context("entry disappeared while loading detail")?;
|
.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`.
|
/// 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`).
|
/// `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>,
|
pub before: Option<String>,
|
||||||
/// Tag full_path filter; includes all entries (root + child) matching the tag subtree
|
/// Tag full_path filter; includes all entries (root + child) matching the tag subtree
|
||||||
pub tag: Option<String>,
|
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`].
|
/// Parses a raw search string into a [`SearchEntriesQuery`].
|
||||||
|
|
@ -498,6 +568,15 @@ pub fn search_entries(
|
||||||
params.push(b.clone());
|
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");
|
sql.push_str(" GROUP BY e.id ORDER BY e.archived_at DESC, e.id DESC");
|
||||||
|
|
||||||
let mut stmt = conn.prepare(&sql)?;
|
let mut stmt = conn.prepare(&sql)?;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue