1use super::listing::{invalid_cursor, validate_cursor_head, validate_directory_cursor};
5use crate::checkpoint::{
6 head_from_manifest, load_basis_metadata_tables, MetadataTableCache, VerifiedMetadataTables,
7 WalTailProjectionCache, WalTailProjectionCacheKey,
8};
9use crate::error::MetadataProjectionLoadError;
10use crate::error::{CoreError, MetadataViewError, Result};
11use crate::metadata::{
12 LeafRevisionPrefetch, MetadataState, MetadataView, MetadataViewSession, ResolvedVisiblePath,
13 RevisionRecord, VisibleChildEntry,
14};
15#[cfg(test)]
16use crate::namespace::basis::read_head_and_metadata_basis;
17use crate::namespace::basis::MetadataBasis;
18use crate::namespace::catalog::VerifiedNamespaceCatalogEntry;
19use crate::path::helpers::{map_path_error_to_core, parse_absolute_path_for_core};
20use crate::storage::content::{content_object_key_for_ref, read_durable_content_bytes};
21use crate::wal::{load_validated_wal_chain, project_validated_wal_tail, WalChainLoadRequest};
22use loonfs_api::wire::control::{HeadState, NamespaceState};
23use loonfs_api::{
24 AbsolutePath, AuthoritativeFileBytes, AuthoritativePathEntry, ChangeSeq, ContentRef,
25 ContentStoreId, DirectoryPageCursor, DisplayName, FileRevision, FileRevisionsPageCursor,
26 InodeId, InodeKind, NamespaceId, Page, PageRequest, RevisionNo, TrashEntry, TrashPageCursor,
27};
28use loonfs_objectstore::ObjectStore;
29use std::sync::Arc;
30use tracing::Instrument;
31
32#[derive(Clone, Copy)]
33pub(crate) enum ReadViewContext<'a> {
34 #[cfg(test)]
37 Latest,
38 PinnedHead {
39 head: &'a HeadState,
40 head_etag: Option<&'a str>,
41 basis: &'a MetadataBasis,
46 },
47}
48
49#[derive(Clone, Copy)]
50pub(crate) struct ReadLoadContext<'a> {
51 view: ReadViewContext<'a>,
52 table_cache: Option<&'a MetadataTableCache>,
53 tail_cache: Option<&'a WalTailProjectionCache>,
54}
55
56impl<'a> ReadLoadContext<'a> {
57 #[cfg(test)]
58 pub(crate) fn latest() -> Self {
59 Self {
60 view: ReadViewContext::Latest,
61 table_cache: None,
62 tail_cache: None,
63 }
64 }
65
66 pub(crate) fn pinned_head(
67 head: &'a HeadState,
68 head_etag: Option<&'a str>,
69 basis: &'a MetadataBasis,
70 table_cache: Option<&'a MetadataTableCache>,
71 tail_cache: Option<&'a WalTailProjectionCache>,
72 ) -> Self {
73 Self {
74 view: ReadViewContext::PinnedHead {
75 head,
76 head_etag,
77 basis,
78 },
79 table_cache,
80 tail_cache,
81 }
82 }
83}
84
85pub(crate) async fn load_metadata_view<'a, S: ObjectStore + ?Sized>(
86 store: &'a S,
87 namespace_id: &NamespaceId,
88 context: ReadLoadContext<'a>,
89) -> Result<LoadedMetadataView<'a, S>> {
90 match context.view {
91 #[cfg(test)]
92 ReadViewContext::Latest => {
93 let loaded = read_head_and_metadata_basis(store, namespace_id)
94 .await
95 .map_err(MetadataProjectionLoadError::LoadHead)?;
96 LoadedMetadataView::load_at_head(
97 store,
98 namespace_id,
99 loaded.head.envelope.state,
100 &loaded.basis,
101 context,
102 )
103 .await
104 }
105 ReadViewContext::PinnedHead { head, basis, .. } => {
106 LoadedMetadataView::load_at_head(store, namespace_id, head.clone(), basis, context)
107 .await
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct DirectDownloadTarget {
120 pub absolute_path: AbsolutePath,
122 pub revision_no: RevisionNo,
124 pub content_ref: ContentRef,
126 pub object_key: String,
128}
129
130pub(crate) struct LoadedMetadataView<'a, S: ObjectStore + ?Sized> {
136 pub(super) namespace_id: NamespaceId,
137 pub(super) content_store_id: ContentStoreId,
138 pub(super) head: HeadState,
139 pub(super) tables: VerifiedMetadataTables<'a, S>,
140 wal_tail_rows: Arc<MetadataState>,
141}
142
143impl<'a, S: ObjectStore + ?Sized> LoadedMetadataView<'a, S> {
144 async fn load_at_head(
145 store: &'a S,
146 namespace_id: &NamespaceId,
147 head: HeadState,
148 basis: &MetadataBasis,
149 load_context: ReadLoadContext<'a>,
150 ) -> Result<Self> {
151 if &head.namespace_id != namespace_id {
152 return Err(CoreError::NamespaceCorrupt(format!(
153 "head namespace `{}` does not match requested namespace `{}`",
154 head.namespace_id, namespace_id
155 )));
156 }
157 if head.state == NamespaceState::Deleted {
158 return Err(CoreError::NamespaceDeleted {
159 namespace_id: namespace_id.clone(),
160 });
161 }
162 let catalog_entry = VerifiedNamespaceCatalogEntry::from_head(&head);
163 let manifest_id = basis.manifest_id();
164 let loaded_basis =
165 load_basis_metadata_tables(store, load_context.table_cache, namespace_id, basis)
166 .await?;
167 let tables = loaded_basis.tables;
168 let manifest_head = head_from_manifest(&head, tables.manifest());
169 let cache_key = match load_context.view {
170 #[cfg(test)]
171 ReadViewContext::Latest => None,
172 ReadViewContext::PinnedHead { head_etag, .. } => {
173 head_etag.map(|etag| WalTailProjectionCacheKey {
174 namespace_id: namespace_id.clone(),
175 manifest_id,
176 manifest_head_seq: manifest_head.seq,
177 head_seq: head.seq,
178 head_etag: etag.to_owned(),
179 })
180 }
181 };
182 if let (Some(cache), Some(key)) = (load_context.tail_cache, cache_key.as_ref()) {
183 if let Some(wal_tail_rows) = cache.get(key) {
184 return Ok(Self {
185 namespace_id: namespace_id.clone(),
186 content_store_id: catalog_entry.content_store_id().clone(),
187 head,
188 tables,
189 wal_tail_rows,
190 });
191 }
192 }
193 let wal_chain = load_validated_wal_chain(
194 store,
195 WalChainLoadRequest {
196 namespace_id,
197 chain_base_seq: manifest_head.seq,
198 head_seq: head.seq,
199 visible_tip: head.visible_wal_tip.clone(),
200 stop_after_seq: None,
201 recent_segments: &head.recent_segments,
202 },
203 )
204 .await
205 .map_err(|error| {
206 CoreError::MetadataProjection(MetadataProjectionLoadError::WalChainLoad(error))
207 })?;
208 let replayed = {
209 let _span =
210 tracing::info_span!("loonfs.phase", phase = "project_metadata_state").entered();
211 project_validated_wal_tail(
212 &manifest_head,
213 &loaded_basis.base_state,
214 Some(head.writer_epoch),
215 &wal_chain,
216 )
217 .map_err(|error| {
218 CoreError::MetadataProjection(MetadataProjectionLoadError::WalReplay(error))
219 })
220 }?;
221 let wal_tail_rows = Arc::new(replayed.resulting_metadata_state);
222 if let (Some(cache), Some(key)) = (load_context.tail_cache, cache_key) {
223 cache.insert(key, Arc::clone(&wal_tail_rows));
224 }
225 Ok(Self {
226 namespace_id: namespace_id.clone(),
227 content_store_id: catalog_entry.content_store_id().clone(),
228 head,
229 tables,
230 wal_tail_rows,
231 })
232 }
233
234 #[tracing::instrument(
235 level = "info",
236 name = "loonfs.phase",
237 err,
238 skip_all,
239 fields(phase = "walk_path")
240 )]
241 pub(crate) async fn resolve_path(&self, absolute_path: &str) -> Result<AuthoritativePathEntry> {
242 let absolute_path = parse_absolute_path_for_core(absolute_path)?;
243 let mut session = self.metadata_view().session();
247 let resolved = session
248 .resolve_visible_path(&absolute_path, LeafRevisionPrefetch::Prefetch)
249 .await?;
250 self.build_authoritative_path_entry_with_session(&mut session, &resolved)
251 .await
252 }
253
254 pub(crate) async fn read_file_bytes(
255 &self,
256 store: &S,
257 absolute_path: &str,
258 max_content_bytes: Option<u64>,
259 ) -> Result<AuthoritativeFileBytes> {
260 let (entry, content_ref) = self.resolve_file_content(absolute_path).await?;
261 ensure_within_read_limit(content_ref.size_bytes, max_content_bytes)?;
262 let read = read_durable_content_bytes(store, &self.content_store_id, &content_ref).await?;
263 Ok(AuthoritativeFileBytes {
264 entry,
265 bytes: read.bytes,
266 })
267 }
268
269 pub(crate) async fn resolve_file_content(
274 &self,
275 absolute_path: &str,
276 ) -> Result<(AuthoritativePathEntry, ContentRef)> {
277 let entry = self.resolve_path(absolute_path).await?;
278 if entry.inode_kind != InodeKind::File {
279 return Err(CoreError::ExpectedFile {
280 path: entry.absolute_path.to_string(),
281 kind: entry.inode_kind,
282 });
283 }
284 let content_ref = entry
285 .content_ref
286 .clone()
287 .ok_or_else(|| CoreError::PathNotFound(absolute_path.to_owned()))?;
288 Ok((entry, content_ref))
289 }
290
291 pub(crate) fn content_store_id(&self) -> &ContentStoreId {
293 &self.content_store_id
294 }
295
296 pub(crate) async fn direct_download_target(
306 &self,
307 absolute_path: &str,
308 revision_no: Option<RevisionNo>,
309 ) -> Result<DirectDownloadTarget> {
310 let entry = self.resolve_path(absolute_path).await?;
311 if entry.inode_kind != InodeKind::File {
312 return Err(CoreError::ExpectedFile {
313 path: entry.absolute_path.to_string(),
314 kind: entry.inode_kind,
315 });
316 }
317 let (revision_no, content_ref) = match revision_no {
318 Some(requested) => {
319 let revision = self.revision_for_inode(entry.inode_id, requested).await?;
320 (revision.revision_no, revision.content_ref)
321 }
322 None => match (entry.revision_no, entry.content_ref) {
325 (Some(revision_no), Some(content_ref)) => (revision_no, content_ref),
326 _ => return Err(CoreError::PathNotFound(absolute_path.to_owned())),
327 },
328 };
329 let object_key = content_object_key_for_ref(&self.content_store_id, &content_ref)?;
330
331 Ok(DirectDownloadTarget {
332 absolute_path: entry.absolute_path,
333 revision_no,
334 content_ref,
335 object_key,
336 })
337 }
338
339 pub(crate) async fn list_file_revisions_page(
340 &self,
341 absolute_path: &str,
342 request: PageRequest<FileRevisionsPageCursor>,
343 ) -> Result<Page<FileRevision, FileRevisionsPageCursor>> {
344 let entry = self.resolve_path(absolute_path).await?;
345 if entry.inode_kind != InodeKind::File {
346 return Err(CoreError::ExpectedFile {
347 path: entry.absolute_path.to_string(),
348 kind: entry.inode_kind,
349 });
350 }
351 self.list_file_revisions_for_inode_page(entry.inode_id, request)
352 .await
353 }
354
355 pub(crate) async fn list_trash_page(
365 &self,
366 request: PageRequest<TrashPageCursor>,
367 ) -> Result<Page<TrashEntry, TrashPageCursor>> {
368 if let Some(cursor) = request.cursor.as_ref() {
369 if cursor.head_seq > self.head.seq {
370 return Err(MetadataViewError::SnapshotUnavailable {
372 requested_seq: cursor.head_seq,
373 head_seq: self.head.seq,
374 }
375 .into());
376 }
377 }
378 let start_after = request
379 .cursor
380 .as_ref()
381 .map(|cursor| (cursor.last_deleted_at_seq, cursor.last_root_inode_id));
382 let mut deletions = self
383 .metadata_view()
384 .session()
385 .active_deletions_page(start_after, request.limit.limit_plus_one())
386 .await?;
387 let has_more = deletions.len() > request.limit.as_usize();
388 if has_more {
389 deletions.truncate(request.limit.as_usize());
390 }
391 let next_cursor = if has_more {
392 let last = deletions
393 .last()
394 .expect("non-zero page limit with more entries must return an item");
395 Some(TrashPageCursor {
396 head_seq: self.head.seq,
397 last_deleted_at_seq: last.deleted_at_seq,
398 last_root_inode_id: last.root_inode_id,
399 })
400 } else {
401 None
402 };
403 let entries = deletions
404 .into_iter()
405 .map(|deletion| TrashEntry {
406 root_inode_id: deletion.root_inode_id,
407 deleted_at_seq: deletion.deleted_at_seq,
408 deleted_at_ms: deletion.deleted_at_ms,
409 parent_inode_id: deletion.parent_inode_id,
410 name_key: deletion.name_key,
411 display_name: deletion.display_name,
412 })
413 .collect();
414 Ok(Page {
415 items: entries,
416 next_cursor,
417 })
418 }
419
420 pub(crate) async fn list_file_revisions_for_inode_page(
421 &self,
422 inode_id: InodeId,
423 request: PageRequest<FileRevisionsPageCursor>,
424 ) -> Result<Page<FileRevision, FileRevisionsPageCursor>> {
425 let inode = self
426 .metadata_view()
427 .inode_at_seq(inode_id)
428 .await?
429 .ok_or_else(|| CoreError::PathNotFound(inode_id.to_string()))?;
430 if inode.inode_kind != InodeKind::File {
431 return Err(CoreError::ExpectedFile {
432 path: inode_id.to_string(),
433 kind: inode.inode_kind,
434 });
435 }
436 if let Some(cursor) = request.cursor.as_ref() {
437 validate_file_revisions_cursor(cursor, self.head.seq, inode_id)?;
438 }
439
440 let start_after = request.cursor.as_ref().map(|cursor| {
441 crate::metadata::manifest_index::RevisionPagePosition::after(
442 cursor.last_revision_no,
443 cursor.last_committed_seq,
444 cursor.last_revision_delta_index,
445 )
446 });
447 let mut revision_records = self
448 .metadata_view()
449 .session()
450 .revisions_for_inode_page_desc(inode_id, start_after, request.limit.limit_plus_one())
451 .await?;
452 let has_more = revision_records.len() > request.limit.as_usize();
453 if has_more {
454 revision_records.truncate(request.limit.as_usize());
455 }
456 let next_cursor = if has_more {
457 let last = revision_records
458 .last()
459 .expect("non-zero page limit with more revisions must return an item");
460 Some(FileRevisionsPageCursor {
461 head_seq: self.head.seq,
462 inode_id,
463 last_revision_no: last.revision_no,
464 last_committed_seq: last.committed_seq,
465 last_revision_delta_index: last.revision_delta_index,
466 })
467 } else {
468 None
469 };
470 let revisions = revision_records
471 .into_iter()
472 .map(|revision| FileRevision {
473 inode_id: revision.inode_id,
474 revision_no: revision.revision_no,
475 committed_seq: revision.committed_seq,
476 committed_at_ms: revision.committed_at_ms,
477 content_ref: revision.content_ref,
478 })
479 .collect();
480
481 Ok(Page {
482 items: revisions,
483 next_cursor,
484 })
485 }
486
487 pub(crate) async fn read_file_revision_bytes(
488 &self,
489 store: &S,
490 absolute_path: &str,
491 revision_no: RevisionNo,
492 max_content_bytes: Option<u64>,
493 ) -> Result<AuthoritativeFileBytes> {
494 let mut entry = self.resolve_path(absolute_path).await?;
495 if entry.inode_kind != InodeKind::File {
496 return Err(CoreError::ExpectedFile {
497 path: entry.absolute_path.to_string(),
498 kind: entry.inode_kind,
499 });
500 }
501 let revision = self.revision_for_inode(entry.inode_id, revision_no).await?;
502 entry.revision_no = Some(revision.revision_no);
503 entry.size_bytes = Some(revision.content_ref.size_bytes);
504 entry.content_ref = Some(revision.content_ref.clone());
505 ensure_within_read_limit(revision.content_ref.size_bytes, max_content_bytes)?;
506 let read = read_durable_content_bytes(store, &self.content_store_id, &revision.content_ref)
507 .await?;
508 Ok(AuthoritativeFileBytes {
509 entry,
510 bytes: read.bytes,
511 })
512 }
513
514 pub(crate) async fn read_file_revision_bytes_for_inode(
515 &self,
516 store: &S,
517 inode_id: InodeId,
518 revision_no: RevisionNo,
519 max_content_bytes: Option<u64>,
520 ) -> Result<Vec<u8>> {
521 let revision = self.revision_for_inode(inode_id, revision_no).await?;
522 ensure_within_read_limit(revision.content_ref.size_bytes, max_content_bytes)?;
523 let read = read_durable_content_bytes(store, &self.content_store_id, &revision.content_ref)
524 .await?;
525 Ok(read.bytes)
526 }
527
528 #[tracing::instrument(
529 level = "info",
530 name = "loonfs.phase",
531 err,
532 skip_all,
533 fields(phase = "walk_path")
534 )]
535 pub(crate) async fn list_path_page(
536 &self,
537 absolute_path: &str,
538 request: PageRequest<DirectoryPageCursor>,
539 ) -> Result<Page<AuthoritativePathEntry, DirectoryPageCursor>> {
540 validate_cursor_head(self.head.seq, request.cursor.as_ref())?;
541
542 let absolute_path = parse_absolute_path_for_core(absolute_path)?;
543 let mut session = self.metadata_view().session();
544 let resolved = session
545 .resolve_visible_path(&absolute_path, LeafRevisionPrefetch::Skip)
546 .await?;
547 if let Some(cursor) = request.cursor.as_ref() {
548 validate_directory_cursor(cursor, &resolved)?;
549 }
550
551 if resolved.inode_kind == InodeKind::File {
552 if request.cursor.is_some() {
553 return Err(invalid_cursor(
554 "directory cursor cannot resume a file listing",
555 ));
556 }
557 return Ok(Page {
558 items: vec![
559 self.build_authoritative_path_entry_with_session(&mut session, &resolved)
560 .await?,
561 ],
562 next_cursor: None,
563 });
564 }
565 if resolved.inode_kind != InodeKind::Directory {
566 return Err(CoreError::ExpectedDirectory {
567 path: resolved.absolute_path,
568 kind: resolved.inode_kind,
569 });
570 }
571
572 let start_after = request
573 .cursor
574 .as_ref()
575 .map(|cursor| cursor.last_name_key.as_str());
576 let select_span = tracing::info_span!(
577 "loonfs.phase",
578 phase = "list_page_select_children",
579 list_page_requested_limit = request.limit.as_usize() as u64,
580 list_page_children_returned = tracing::field::Empty,
581 );
582 let mut children = session
583 .visible_children_page_by_name_key(
584 resolved.inode_id,
585 start_after,
586 request.limit.limit_plus_one(),
587 )
588 .instrument(select_span.clone())
589 .await?;
590 select_span.record("list_page_children_returned", children.len() as u64);
591 let has_more = children.len() > request.limit.as_usize();
592 if has_more {
593 children.truncate(request.limit.as_usize());
594 }
595
596 let next_cursor = if has_more {
597 let last = children
598 .last()
599 .expect("non-zero page limit with more children must return an item");
600 Some(DirectoryPageCursor {
601 head_seq: self.head.seq,
602 directory_inode_id: resolved.inode_id,
603 last_name_key: last.binding.name_key.clone(),
604 })
605 } else {
606 None
607 };
608
609 let build_span = tracing::info_span!(
610 "loonfs.phase",
611 phase = "list_page_build_entries",
612 list_page_children_returned = children.len() as u64,
613 list_page_visible_child_calls = tracing::field::Empty,
614 list_page_visible_inode_calls = tracing::field::Empty,
615 list_page_current_parent_binding_calls = tracing::field::Empty,
616 list_page_covering_tombstone_calls = tracing::field::Empty,
617 list_page_latest_revision_calls = tracing::field::Empty,
618 list_page_direntry_child_scan_calls = tracing::field::Empty,
619 list_page_scan_prefix_calls = tracing::field::Empty,
620 list_page_scan_range_page_calls = tracing::field::Empty,
621 list_page_preload_unbind_range_scans = tracing::field::Empty,
622 list_page_preload_child_lookups = tracing::field::Empty,
623 );
624 let entries = async {
625 let mut entries = Vec::with_capacity(children.len());
626 for child in children {
627 entries.push(
628 self.build_authoritative_path_entry_from_visible_child(
629 &mut session,
630 &resolved,
631 child,
632 )
633 .await?,
634 );
635 }
636 Ok::<_, CoreError>(entries)
637 }
638 .instrument(build_span.clone())
639 .await?;
640 let counters = session.counters();
641 build_span.record(
642 "list_page_visible_child_calls",
643 counters.visible_child_calls,
644 );
645 build_span.record(
646 "list_page_visible_inode_calls",
647 counters.visible_inode_calls,
648 );
649 build_span.record(
650 "list_page_current_parent_binding_calls",
651 counters.current_parent_binding_calls,
652 );
653 build_span.record(
654 "list_page_covering_tombstone_calls",
655 counters.covering_tombstone_calls,
656 );
657 build_span.record(
658 "list_page_latest_revision_calls",
659 counters.latest_revision_calls,
660 );
661 build_span.record(
662 "list_page_direntry_child_scan_calls",
663 counters.direntry_child_scan_calls,
664 );
665 build_span.record("list_page_scan_prefix_calls", counters.scan_prefix_calls);
666 build_span.record(
667 "list_page_scan_range_page_calls",
668 counters.scan_range_page_calls,
669 );
670 build_span.record(
671 "list_page_preload_unbind_range_scans",
672 counters.list_preload_unbind_range_scans,
673 );
674 build_span.record(
675 "list_page_preload_child_lookups",
676 counters.list_preload_child_lookups,
677 );
678
679 Ok(Page {
680 items: entries,
681 next_cursor,
682 })
683 }
684
685 async fn build_authoritative_path_entry_with_session(
689 &self,
690 session: &mut MetadataViewSession<'_, '_, S>,
691 resolved: &ResolvedVisiblePath,
692 ) -> Result<AuthoritativePathEntry> {
693 let revision = if resolved.inode_kind == InodeKind::File {
694 session
695 .latest_revision_head_of_visible(resolved.inode_id)
696 .await?
697 } else {
698 None
699 };
700 let content_ref = revision
701 .as_ref()
702 .map(|revision| revision.content_ref.clone());
703 let size_bytes = content_ref
704 .as_ref()
705 .map(|content_ref| content_ref.size_bytes);
706 let absolute_path = AbsolutePath::parse(&resolved.absolute_path).map_err(|error| {
707 CoreError::NamespaceCorrupt(format!(
708 "resolved visible path `{}` is not a valid absolute path: {error}",
709 resolved.absolute_path
710 ))
711 })?;
712 let display_name = resolved
713 .parent_inode_id
714 .map(|_| {
715 DisplayName::parse(&resolved.display_name).map_err(|error| {
716 CoreError::NamespaceCorrupt(format!(
717 "stored display name for inode `{}` is invalid: {error}",
718 resolved.inode_id
719 ))
720 })
721 })
722 .transpose()?;
723 Ok(AuthoritativePathEntry {
724 namespace_id: self.namespace_id.clone(),
725 absolute_path,
726 inode_id: resolved.inode_id,
727 inode_kind: resolved.inode_kind,
728 head_seq: self.head.seq,
729 parent_inode_id: resolved.parent_inode_id,
730 display_name,
731 revision_no: revision.as_ref().map(|revision| revision.revision_no),
732 size_bytes,
733 content_ref,
734 committed_at_ms: revision.as_ref().map(|revision| revision.committed_at_ms),
735 })
736 }
737
738 async fn build_authoritative_path_entry_from_visible_child(
739 &self,
740 session: &mut MetadataViewSession<'_, '_, S>,
741 resolved_dir: &ResolvedVisiblePath,
742 child: VisibleChildEntry,
743 ) -> Result<AuthoritativePathEntry> {
744 let child_path = AbsolutePath::parse(&resolved_dir.absolute_path)
745 .map_err(map_path_error_to_core)?
746 .join(&child.binding.display_name);
747 self.build_authoritative_path_entry_with_session(
748 session,
749 &ResolvedVisiblePath {
750 absolute_path: child_path.as_str().to_owned(),
751 inode_id: child.binding.child_inode_id,
752 inode_kind: child.inode.inode_kind,
753 parent_inode_id: Some(child.binding.parent_inode_id),
754 display_name: child.binding.display_name.to_string(),
755 },
756 )
757 .await
758 }
759
760 async fn revision_for_inode(
761 &self,
762 inode_id: InodeId,
763 revision_no: RevisionNo,
764 ) -> Result<RevisionRecord> {
765 self.metadata_view()
766 .revision_for_inode(inode_id, revision_no)
767 .await
768 }
769
770 pub(super) fn metadata_view(&self) -> MetadataView<'_, '_, S> {
771 MetadataView::from_loaded_head(&self.head, &self.tables, self.wal_tail_rows.as_ref())
772 }
773}
774
775pub(crate) fn ensure_within_read_limit(
779 size_bytes: u64,
780 max_content_bytes: Option<u64>,
781) -> Result<()> {
782 match max_content_bytes {
783 Some(max_bytes) if size_bytes > max_bytes => Err(CoreError::ContentTooLarge {
784 size_bytes,
785 max_bytes,
786 }),
787 _ => Ok(()),
788 }
789}
790
791fn validate_file_revisions_cursor(
792 cursor: &FileRevisionsPageCursor,
793 head_seq: ChangeSeq,
794 inode_id: InodeId,
795) -> Result<()> {
796 if cursor.head_seq > head_seq {
797 return Err(MetadataViewError::SnapshotUnavailable {
802 requested_seq: cursor.head_seq,
803 head_seq,
804 }
805 .into());
806 }
807 if cursor.inode_id != inode_id {
808 return Err(invalid_cursor(
809 "file revisions cursor inode does not match the requested file",
810 ));
811 }
812 Ok(())
813}