use super::materialized_view::LoadedMetadataView;
use crate::error::{CoreError, Result};
use crate::metadata::MetadataViewSession;
use loonfs_api::{AbsolutePath, InodeId, InodeKind, RevisionNo, ROOT_INODE_ID};
use loonfs_objectstore::ObjectStore;
use std::collections::{HashMap, HashSet};
pub const MAX_RESOLVE_CURRENT_FILES: usize = loonfs_api::DEFAULT_MAX_PAGE_LIMIT as usize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CurrentFileState {
pub inode_id: InodeId,
pub visible: bool,
pub current_revision_no: Option<RevisionNo>,
pub current_path: Option<AbsolutePath>,
}
pub(crate) fn ensure_resolve_batch_within_cap(requested: usize) -> Result<()> {
if requested > MAX_RESOLVE_CURRENT_FILES {
return Err(CoreError::BatchTooLarge {
requested,
max: MAX_RESOLVE_CURRENT_FILES,
});
}
Ok(())
}
pub(crate) async fn resolve_current_files<S: ObjectStore + ?Sized>(
view: &LoadedMetadataView<'_, S>,
inode_ids: &[InodeId],
) -> Result<Vec<CurrentFileState>> {
ensure_resolve_batch_within_cap(inode_ids.len())?;
let mut session = view.metadata_view().session();
let mut ancestor_paths = HashMap::new();
let mut states = Vec::with_capacity(inode_ids.len());
for &inode_id in inode_ids {
states.push(resolve_one(&mut session, &mut ancestor_paths, inode_id).await?);
}
Ok(states)
}
async fn resolve_one<S: ObjectStore + ?Sized>(
session: &mut MetadataViewSession<'_, '_, S>,
ancestor_paths: &mut HashMap<InodeId, AbsolutePath>,
inode_id: InodeId,
) -> Result<CurrentFileState> {
let Some(inode) = session.visible_inode(inode_id).await? else {
return Ok(missing(inode_id));
};
let Some(current_path) = current_path(session, ancestor_paths, inode_id).await? else {
return Ok(missing(inode_id));
};
let current_revision_no = if inode.inode_kind == InodeKind::File {
session
.latest_revision_head_of_visible(inode_id)
.await?
.map(|revision| revision.revision_no)
} else {
None
};
Ok(CurrentFileState {
inode_id,
visible: true,
current_revision_no,
current_path: Some(current_path),
})
}
fn missing(inode_id: InodeId) -> CurrentFileState {
CurrentFileState {
inode_id,
visible: false,
current_revision_no: None,
current_path: None,
}
}
async fn current_path<S: ObjectStore + ?Sized>(
session: &mut MetadataViewSession<'_, '_, S>,
ancestor_paths: &mut HashMap<InodeId, AbsolutePath>,
inode_id: InodeId,
) -> Result<Option<AbsolutePath>> {
let mut climbed = Vec::new();
let mut visited = HashSet::new();
let mut current = inode_id;
let base = loop {
if current == ROOT_INODE_ID {
break AbsolutePath::root();
}
if let Some(known) = ancestor_paths.get(¤t) {
break known.clone();
}
if !visited.insert(current) {
return Ok(None);
}
let Some(binding) = session.current_parent_binding_for_child(current).await? else {
return Ok(None);
};
current = binding.parent_inode_id;
climbed.push(binding);
};
let mut path = base;
for binding in climbed.iter().rev() {
path = path.join(&binding.display_name);
ancestor_paths.insert(binding.child_inode_id, path.clone());
}
Ok(Some(path))
}
#[cfg(test)]
mod tests {
use super::{ensure_resolve_batch_within_cap, MAX_RESOLVE_CURRENT_FILES};
use loonfs_api::{ErrorCode, PaginationPolicy};
#[test]
fn the_batch_cap_is_the_pagination_maximum() {
assert_eq!(
MAX_RESOLVE_CURRENT_FILES,
PaginationPolicy::default().max_limit().get() as usize,
"the batch cap is the page limit, not a second number"
);
}
#[test]
fn a_batch_at_the_cap_is_accepted_and_one_past_it_names_the_cap() {
assert!(ensure_resolve_batch_within_cap(MAX_RESOLVE_CURRENT_FILES).is_ok());
let error = ensure_resolve_batch_within_cap(MAX_RESOLVE_CURRENT_FILES + 1)
.expect_err("one past the cap is refused");
assert_eq!(error.code(), ErrorCode::InvalidRequest);
assert!(
error
.to_string()
.contains(&MAX_RESOLVE_CURRENT_FILES.to_string()),
"the refusal should name the cap: {error}"
);
}
}