use crate::authz::{KbAccess, ScanScope, effective_prefix};
use notedthat_core::{
KbSlug, KeyFilter, ObjectMeta, ObjectPath, Rollup, Storage, StorageError, Verb,
is_internal_path, roll_up,
};
pub(super) const BROWSE_MAX_KEYS: usize = 10_000;
const SCAN_PAGE: u32 = 1000;
pub(super) struct DirectoryListing {
pub(super) rollup: Rollup,
pub(super) truncated: bool,
pub(super) last_key: Option<String>,
}
pub(super) async fn read_directory(
storage: &dyn Storage,
kb: &KbSlug,
prefix: &str,
access: &KbAccess,
) -> Result<DirectoryListing, StorageError> {
let filter = access.filter(Verb::List);
let requested = (!prefix.is_empty()).then_some(prefix);
let scan_prefix = match effective_prefix(requested, filter.literal_prefix_hint()) {
ScanScope::From(scan) => scan,
ScanScope::Disjoint => {
return Ok(DirectoryListing {
rollup: roll_up(Vec::new(), prefix),
truncated: false,
last_key: None,
});
}
};
let mut visible: Vec<ObjectMeta> = Vec::new();
let mut cursor: Option<String> = None;
let mut scanned = 0_usize;
let mut truncated = false;
let mut last_key = None;
loop {
let page = storage
.list_objects(kb, scan_prefix.as_deref(), SCAN_PAGE, cursor.as_deref())
.await?;
for object in page.objects {
scanned += 1;
if scanned > BROWSE_MAX_KEYS {
truncated = true;
break;
}
last_key = Some(object.key.clone());
if is_visible(&object.key, &filter) {
visible.push(object);
}
}
if truncated {
break;
}
let Some(next) = page.next_cursor else { break };
if cursor.as_deref() == Some(next.as_str()) {
return Err(StorageError::BackendUnavailable {
message: "storage returned a non-advancing cursor".into(),
});
}
cursor = Some(next);
}
Ok(DirectoryListing {
rollup: roll_up(visible, prefix),
truncated,
last_key,
})
}
fn is_visible(key: &str, filter: &KeyFilter<'_>) -> bool {
if is_internal_path(key) {
return false;
}
if ObjectPath::try_from_str(key).is_err() {
tracing::debug!(key, "browse: omitting an unrepresentable object key");
return false;
}
filter.allows(key)
}
pub(super) fn is_present(listing: &DirectoryListing) -> bool {
!listing.rollup.is_empty() || listing.truncated
}